mirror of
https://github.com/spf13/viper
synced 2024-12-22 11:37:02 +00:00
feat(encoding): add ini codec
Signed-off-by: Mark Sagi-Kazar <mark.sagikazar@gmail.com>
This commit is contained in:
parent
e1924e3858
commit
38a4fbd769
3 changed files with 285 additions and 0 deletions
99
internal/encoding/ini/codec.go
Normal file
99
internal/encoding/ini/codec.go
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
package ini
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/spf13/cast"
|
||||||
|
"gopkg.in/ini.v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LoadOptions contains all customized options used for load data source(s).
|
||||||
|
// This type is added here for convenience: this way consumers can import a single package called "ini".
|
||||||
|
type LoadOptions = ini.LoadOptions
|
||||||
|
|
||||||
|
// Codec implements the encoding.Encoder and encoding.Decoder interfaces for INI encoding.
|
||||||
|
type Codec struct {
|
||||||
|
KeyDelimiter string
|
||||||
|
LoadOptions LoadOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Codec) Encode(v map[string]interface{}) ([]byte, error) {
|
||||||
|
cfg := ini.Empty()
|
||||||
|
ini.PrettyFormat = false
|
||||||
|
|
||||||
|
flattened := map[string]interface{}{}
|
||||||
|
|
||||||
|
flattened = flattenAndMergeMap(flattened, v, "", c.keyDelimiter())
|
||||||
|
|
||||||
|
keys := make([]string, 0, len(flattened))
|
||||||
|
|
||||||
|
for key := range flattened {
|
||||||
|
keys = append(keys, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Strings(keys)
|
||||||
|
|
||||||
|
for _, key := range keys {
|
||||||
|
sectionName, keyName := "", key
|
||||||
|
|
||||||
|
lastSep := strings.LastIndex(key, ".")
|
||||||
|
if lastSep != -1 {
|
||||||
|
sectionName = key[:(lastSep)]
|
||||||
|
keyName = key[(lastSep + 1):]
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: is this a good idea?
|
||||||
|
if sectionName == "default" {
|
||||||
|
sectionName = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.Section(sectionName).Key(keyName).SetValue(cast.ToString(flattened[key]))
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
|
||||||
|
_, err := cfg.WriteTo(&buf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Codec) Decode(b []byte, v map[string]interface{}) error {
|
||||||
|
cfg := ini.Empty(c.LoadOptions)
|
||||||
|
|
||||||
|
err := cfg.Append(b)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
sections := cfg.Sections()
|
||||||
|
|
||||||
|
for i := 0; i < len(sections); i++ {
|
||||||
|
section := sections[i]
|
||||||
|
keys := section.Keys()
|
||||||
|
|
||||||
|
for j := 0; j < len(keys); j++ {
|
||||||
|
key := keys[j]
|
||||||
|
value := cfg.Section(section.Name()).Key(key.Name()).String()
|
||||||
|
|
||||||
|
deepestMap := deepSearch(v, strings.Split(section.Name(), c.keyDelimiter()))
|
||||||
|
|
||||||
|
// set innermost value
|
||||||
|
deepestMap[key.Name()] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Codec) keyDelimiter() string {
|
||||||
|
if c.KeyDelimiter == "" {
|
||||||
|
return "."
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.KeyDelimiter
|
||||||
|
}
|
112
internal/encoding/ini/codec_test.go
Normal file
112
internal/encoding/ini/codec_test.go
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
package ini
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// original form of the data
|
||||||
|
const original = `; key-value pair
|
||||||
|
key=value ; key-value pair
|
||||||
|
|
||||||
|
# map
|
||||||
|
[map] # map
|
||||||
|
key=%(key)s
|
||||||
|
|
||||||
|
`
|
||||||
|
|
||||||
|
// encoded form of the data
|
||||||
|
const encoded = `key=value
|
||||||
|
|
||||||
|
[map]
|
||||||
|
key=value
|
||||||
|
|
||||||
|
`
|
||||||
|
|
||||||
|
// decoded form of the data
|
||||||
|
//
|
||||||
|
// in case of INI it's slightly different from Viper's internal representation
|
||||||
|
// (eg. top level keys land in a section called default)
|
||||||
|
var decoded = map[string]interface{}{
|
||||||
|
"DEFAULT": map[string]interface{}{
|
||||||
|
"key": "value",
|
||||||
|
},
|
||||||
|
"map": map[string]interface{}{
|
||||||
|
"key": "value",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Viper's internal representation
|
||||||
|
var data = map[string]interface{}{
|
||||||
|
"key": "value",
|
||||||
|
"map": map[string]interface{}{
|
||||||
|
"key": "value",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCodec_Encode(t *testing.T) {
|
||||||
|
t.Run("OK", func(t *testing.T) {
|
||||||
|
codec := Codec{}
|
||||||
|
|
||||||
|
b, err := codec.Encode(data)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if encoded != string(b) {
|
||||||
|
t.Fatalf("decoded value does not match the expected one\nactual: %#v\nexpected: %#v", string(b), encoded)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Default", func(t *testing.T) {
|
||||||
|
codec := Codec{}
|
||||||
|
|
||||||
|
data := map[string]interface{}{
|
||||||
|
"default": map[string]interface{}{
|
||||||
|
"key": "value",
|
||||||
|
},
|
||||||
|
"map": map[string]interface{}{
|
||||||
|
"key": "value",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := codec.Encode(data)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if encoded != string(b) {
|
||||||
|
t.Fatalf("decoded value does not match the expected one\nactual: %#v\nexpected: %#v", string(b), encoded)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCodec_Decode(t *testing.T) {
|
||||||
|
t.Run("OK", func(t *testing.T) {
|
||||||
|
codec := Codec{}
|
||||||
|
|
||||||
|
v := map[string]interface{}{}
|
||||||
|
|
||||||
|
err := codec.Decode([]byte(original), v)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(decoded, v) {
|
||||||
|
t.Fatalf("decoded value does not match the expected one\nactual: %#v\nexpected: %#v", v, decoded)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("InvalidData", func(t *testing.T) {
|
||||||
|
codec := Codec{}
|
||||||
|
|
||||||
|
v := map[string]interface{}{}
|
||||||
|
|
||||||
|
err := codec.Decode([]byte(`invalid data`), v)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected decoding to fail")
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("decoding failed as expected: %s", err)
|
||||||
|
})
|
||||||
|
}
|
74
internal/encoding/ini/map_utils.go
Normal file
74
internal/encoding/ini/map_utils.go
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
package ini
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/spf13/cast"
|
||||||
|
)
|
||||||
|
|
||||||
|
// THIS CODE IS COPIED HERE: IT SHOULD NOT BE MODIFIED
|
||||||
|
// AT SOME POINT IT WILL BE MOVED TO A COMMON PLACE
|
||||||
|
// deepSearch scans deep maps, following the key indexes listed in the
|
||||||
|
// sequence "path".
|
||||||
|
// The last value is expected to be another map, and is returned.
|
||||||
|
//
|
||||||
|
// In case intermediate keys do not exist, or map to a non-map value,
|
||||||
|
// a new map is created and inserted, and the search continues from there:
|
||||||
|
// the initial map "m" may be modified!
|
||||||
|
func deepSearch(m map[string]interface{}, path []string) map[string]interface{} {
|
||||||
|
for _, k := range path {
|
||||||
|
m2, ok := m[k]
|
||||||
|
if !ok {
|
||||||
|
// intermediate key does not exist
|
||||||
|
// => create it and continue from there
|
||||||
|
m3 := make(map[string]interface{})
|
||||||
|
m[k] = m3
|
||||||
|
m = m3
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
m3, ok := m2.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
// intermediate key is a value
|
||||||
|
// => replace with a new map
|
||||||
|
m3 = make(map[string]interface{})
|
||||||
|
m[k] = m3
|
||||||
|
}
|
||||||
|
// continue search from here
|
||||||
|
m = m3
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
// flattenAndMergeMap recursively flattens the given map into a new map
|
||||||
|
// Code is based on the function with the same name in tha main package.
|
||||||
|
// TODO: move it to a common place
|
||||||
|
func flattenAndMergeMap(shadow map[string]interface{}, m map[string]interface{}, prefix string, delimiter string) map[string]interface{} {
|
||||||
|
if shadow != nil && prefix != "" && shadow[prefix] != nil {
|
||||||
|
// prefix is shadowed => nothing more to flatten
|
||||||
|
return shadow
|
||||||
|
}
|
||||||
|
if shadow == nil {
|
||||||
|
shadow = make(map[string]interface{})
|
||||||
|
}
|
||||||
|
|
||||||
|
var m2 map[string]interface{}
|
||||||
|
if prefix != "" {
|
||||||
|
prefix += delimiter
|
||||||
|
}
|
||||||
|
for k, val := range m {
|
||||||
|
fullKey := prefix + k
|
||||||
|
switch val.(type) {
|
||||||
|
case map[string]interface{}:
|
||||||
|
m2 = val.(map[string]interface{})
|
||||||
|
case map[interface{}]interface{}:
|
||||||
|
m2 = cast.ToStringMap(val)
|
||||||
|
default:
|
||||||
|
// immediate value
|
||||||
|
shadow[strings.ToLower(fullKey)] = val
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// recursively merge to shadow map
|
||||||
|
shadow = flattenAndMergeMap(shadow, m2, fullKey, delimiter)
|
||||||
|
}
|
||||||
|
return shadow
|
||||||
|
}
|
Loading…
Reference in a new issue