mirror of
https://github.com/spf13/viper
synced 2024-12-22 11:37:02 +00:00
refactor(encoding): remove external encoding libraries
Signed-off-by: Mark Sagi-Kazar <mark.sagikazar@gmail.com>
This commit is contained in:
parent
f98411d629
commit
517cbc6315
16 changed files with 6 additions and 1097 deletions
12
go.mod
12
go.mod
|
@ -11,26 +11,26 @@ require (
|
||||||
github.com/go-viper/encoding/toml v0.1.0
|
github.com/go-viper/encoding/toml v0.1.0
|
||||||
github.com/go-viper/encoding/yaml v0.1.0
|
github.com/go-viper/encoding/yaml v0.1.0
|
||||||
github.com/go-viper/mapstructure/v2 v2.0.0
|
github.com/go-viper/mapstructure/v2 v2.0.0
|
||||||
github.com/hashicorp/hcl v1.0.0
|
|
||||||
github.com/magiconair/properties v1.8.7
|
|
||||||
github.com/pelletier/go-toml/v2 v2.2.2
|
|
||||||
github.com/sagikazarmark/locafero v0.6.0
|
github.com/sagikazarmark/locafero v0.6.0
|
||||||
github.com/spf13/afero v1.11.0
|
github.com/spf13/afero v1.11.0
|
||||||
github.com/spf13/cast v1.6.0
|
github.com/spf13/cast v1.6.0
|
||||||
github.com/spf13/pflag v1.0.5
|
github.com/spf13/pflag v1.0.5
|
||||||
github.com/stretchr/testify v1.9.0
|
github.com/stretchr/testify v1.9.0
|
||||||
github.com/subosito/gotenv v1.6.0
|
|
||||||
gopkg.in/ini.v1 v1.67.0
|
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/google/go-cmp v0.6.0 // indirect
|
github.com/google/go-cmp v0.6.0 // indirect
|
||||||
|
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||||
|
github.com/magiconair/properties v1.8.7 // indirect
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||||
|
github.com/subosito/gotenv v1.6.0 // indirect
|
||||||
go.uber.org/atomic v1.9.0 // indirect
|
go.uber.org/atomic v1.9.0 // indirect
|
||||||
go.uber.org/multierr v1.9.0 // indirect
|
go.uber.org/multierr v1.9.0 // indirect
|
||||||
golang.org/x/sys v0.20.0 // indirect
|
golang.org/x/sys v0.20.0 // indirect
|
||||||
golang.org/x/text v0.15.0 // indirect
|
golang.org/x/text v0.15.0 // indirect
|
||||||
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,61 +0,0 @@
|
||||||
package dotenv
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/subosito/gotenv"
|
|
||||||
)
|
|
||||||
|
|
||||||
const keyDelimiter = "_"
|
|
||||||
|
|
||||||
// Codec implements the encoding.Encoder and encoding.Decoder interfaces for encoding data containing environment variables
|
|
||||||
// (commonly called as dotenv format).
|
|
||||||
type Codec struct{}
|
|
||||||
|
|
||||||
func (Codec) Encode(v map[string]any) ([]byte, error) {
|
|
||||||
flattened := map[string]any{}
|
|
||||||
|
|
||||||
flattened = flattenAndMergeMap(flattened, v, "", keyDelimiter)
|
|
||||||
|
|
||||||
keys := make([]string, 0, len(flattened))
|
|
||||||
|
|
||||||
for key := range flattened {
|
|
||||||
keys = append(keys, key)
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Strings(keys)
|
|
||||||
|
|
||||||
var buf bytes.Buffer
|
|
||||||
|
|
||||||
for _, key := range keys {
|
|
||||||
_, err := buf.WriteString(fmt.Sprintf("%v=%v\n", strings.ToUpper(key), flattened[key]))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return buf.Bytes(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (Codec) Decode(b []byte, v map[string]any) error {
|
|
||||||
var buf bytes.Buffer
|
|
||||||
|
|
||||||
_, err := buf.Write(b)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
env, err := gotenv.StrictParse(&buf)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for key, value := range env {
|
|
||||||
v[key] = value
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -1,55 +0,0 @@
|
||||||
package dotenv
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
// original form of the data.
|
|
||||||
const original = `# key-value pair
|
|
||||||
KEY=value
|
|
||||||
`
|
|
||||||
|
|
||||||
// encoded form of the data.
|
|
||||||
const encoded = `KEY=value
|
|
||||||
`
|
|
||||||
|
|
||||||
// data is Viper's internal representation.
|
|
||||||
var data = map[string]any{
|
|
||||||
"KEY": "value",
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCodec_Encode(t *testing.T) {
|
|
||||||
codec := Codec{}
|
|
||||||
|
|
||||||
b, err := codec.Encode(data)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
assert.Equal(t, encoded, string(b))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCodec_Decode(t *testing.T) {
|
|
||||||
t.Run("OK", func(t *testing.T) {
|
|
||||||
codec := Codec{}
|
|
||||||
|
|
||||||
v := map[string]any{}
|
|
||||||
|
|
||||||
err := codec.Decode([]byte(original), v)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
assert.Equal(t, data, v)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("InvalidData", func(t *testing.T) {
|
|
||||||
codec := Codec{}
|
|
||||||
|
|
||||||
v := map[string]any{}
|
|
||||||
|
|
||||||
err := codec.Decode([]byte(`invalid data`), v)
|
|
||||||
require.Error(t, err)
|
|
||||||
|
|
||||||
t.Logf("decoding failed as expected: %s", err)
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,41 +0,0 @@
|
||||||
package dotenv
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/spf13/cast"
|
|
||||||
)
|
|
||||||
|
|
||||||
// flattenAndMergeMap recursively flattens the given map into a new map
|
|
||||||
// Code is based on the function with the same name in the main package.
|
|
||||||
// TODO: move it to a common place.
|
|
||||||
func flattenAndMergeMap(shadow, m map[string]any, prefix, delimiter string) map[string]any {
|
|
||||||
if shadow != nil && prefix != "" && shadow[prefix] != nil {
|
|
||||||
// prefix is shadowed => nothing more to flatten
|
|
||||||
return shadow
|
|
||||||
}
|
|
||||||
if shadow == nil {
|
|
||||||
shadow = make(map[string]any)
|
|
||||||
}
|
|
||||||
|
|
||||||
var m2 map[string]any
|
|
||||||
if prefix != "" {
|
|
||||||
prefix += delimiter
|
|
||||||
}
|
|
||||||
for k, val := range m {
|
|
||||||
fullKey := prefix + k
|
|
||||||
switch val := val.(type) {
|
|
||||||
case map[string]any:
|
|
||||||
m2 = val
|
|
||||||
case map[any]any:
|
|
||||||
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
|
|
||||||
}
|
|
|
@ -1,40 +0,0 @@
|
||||||
package hcl
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
|
|
||||||
"github.com/hashicorp/hcl"
|
|
||||||
"github.com/hashicorp/hcl/hcl/printer"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Codec implements the encoding.Encoder and encoding.Decoder interfaces for HCL encoding.
|
|
||||||
// TODO: add printer config to the codec?
|
|
||||||
type Codec struct{}
|
|
||||||
|
|
||||||
func (Codec) Encode(v map[string]any) ([]byte, error) {
|
|
||||||
b, err := json.Marshal(v)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: use printer.Format? Is the trailing newline an issue?
|
|
||||||
|
|
||||||
ast, err := hcl.Parse(string(b))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var buf bytes.Buffer
|
|
||||||
|
|
||||||
err = printer.Fprint(&buf, ast.Node)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return buf.Bytes(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (Codec) Decode(b []byte, v map[string]any) error {
|
|
||||||
return hcl.Unmarshal(b, &v)
|
|
||||||
}
|
|
|
@ -1,132 +0,0 @@
|
||||||
package hcl
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
// original form of the data.
|
|
||||||
const original = `# key-value pair
|
|
||||||
"key" = "value"
|
|
||||||
|
|
||||||
// list
|
|
||||||
"list" = ["item1", "item2", "item3"]
|
|
||||||
|
|
||||||
/* map */
|
|
||||||
"map" = {
|
|
||||||
"key" = "value"
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
nested map
|
|
||||||
*/
|
|
||||||
"nested_map" "map" {
|
|
||||||
"key" = "value"
|
|
||||||
|
|
||||||
"list" = ["item1", "item2", "item3"]
|
|
||||||
}`
|
|
||||||
|
|
||||||
// encoded form of the data.
|
|
||||||
const encoded = `"key" = "value"
|
|
||||||
|
|
||||||
"list" = ["item1", "item2", "item3"]
|
|
||||||
|
|
||||||
"map" = {
|
|
||||||
"key" = "value"
|
|
||||||
}
|
|
||||||
|
|
||||||
"nested_map" "map" {
|
|
||||||
"key" = "value"
|
|
||||||
|
|
||||||
"list" = ["item1", "item2", "item3"]
|
|
||||||
}`
|
|
||||||
|
|
||||||
// decoded form of the data.
|
|
||||||
//
|
|
||||||
// In case of HCL it's slightly different from Viper's internal representation
|
|
||||||
// (e.g. map is decoded into a list of maps).
|
|
||||||
var decoded = map[string]any{
|
|
||||||
"key": "value",
|
|
||||||
"list": []any{
|
|
||||||
"item1",
|
|
||||||
"item2",
|
|
||||||
"item3",
|
|
||||||
},
|
|
||||||
"map": []map[string]any{
|
|
||||||
{
|
|
||||||
"key": "value",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"nested_map": []map[string]any{
|
|
||||||
{
|
|
||||||
"map": []map[string]any{
|
|
||||||
{
|
|
||||||
"key": "value",
|
|
||||||
"list": []any{
|
|
||||||
"item1",
|
|
||||||
"item2",
|
|
||||||
"item3",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// data is Viper's internal representation.
|
|
||||||
var data = map[string]any{
|
|
||||||
"key": "value",
|
|
||||||
"list": []any{
|
|
||||||
"item1",
|
|
||||||
"item2",
|
|
||||||
"item3",
|
|
||||||
},
|
|
||||||
"map": map[string]any{
|
|
||||||
"key": "value",
|
|
||||||
},
|
|
||||||
"nested_map": map[string]any{
|
|
||||||
"map": map[string]any{
|
|
||||||
"key": "value",
|
|
||||||
"list": []any{
|
|
||||||
"item1",
|
|
||||||
"item2",
|
|
||||||
"item3",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCodec_Encode(t *testing.T) {
|
|
||||||
codec := Codec{}
|
|
||||||
|
|
||||||
b, err := codec.Encode(data)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
assert.Equal(t, encoded, string(b))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCodec_Decode(t *testing.T) {
|
|
||||||
t.Run("OK", func(t *testing.T) {
|
|
||||||
codec := Codec{}
|
|
||||||
|
|
||||||
v := map[string]any{}
|
|
||||||
|
|
||||||
err := codec.Decode([]byte(original), v)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
assert.Equal(t, decoded, v)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("InvalidData", func(t *testing.T) {
|
|
||||||
codec := Codec{}
|
|
||||||
|
|
||||||
v := map[string]any{}
|
|
||||||
|
|
||||||
err := codec.Decode([]byte(`invalid data`), v)
|
|
||||||
require.Error(t, err)
|
|
||||||
|
|
||||||
t.Logf("decoding failed as expected: %s", err)
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,99 +0,0 @@
|
||||||
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]any) ([]byte, error) {
|
|
||||||
cfg := ini.Empty()
|
|
||||||
ini.PrettyFormat = false
|
|
||||||
|
|
||||||
flattened := map[string]any{}
|
|
||||||
|
|
||||||
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]any) 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
|
|
||||||
}
|
|
|
@ -1,99 +0,0 @@
|
||||||
package ini
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
// 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
|
|
||||||
// (e.g. top level keys land in a section called default).
|
|
||||||
var decoded = map[string]any{
|
|
||||||
"DEFAULT": map[string]any{
|
|
||||||
"key": "value",
|
|
||||||
},
|
|
||||||
"map": map[string]any{
|
|
||||||
"key": "value",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// data is Viper's internal representation.
|
|
||||||
var data = map[string]any{
|
|
||||||
"key": "value",
|
|
||||||
"map": map[string]any{
|
|
||||||
"key": "value",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCodec_Encode(t *testing.T) {
|
|
||||||
t.Run("OK", func(t *testing.T) {
|
|
||||||
codec := Codec{}
|
|
||||||
|
|
||||||
b, err := codec.Encode(data)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
assert.Equal(t, encoded, string(b))
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("Default", func(t *testing.T) {
|
|
||||||
codec := Codec{}
|
|
||||||
|
|
||||||
data := map[string]any{
|
|
||||||
"default": map[string]any{
|
|
||||||
"key": "value",
|
|
||||||
},
|
|
||||||
"map": map[string]any{
|
|
||||||
"key": "value",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
b, err := codec.Encode(data)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
assert.Equal(t, encoded, string(b))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCodec_Decode(t *testing.T) {
|
|
||||||
t.Run("OK", func(t *testing.T) {
|
|
||||||
codec := Codec{}
|
|
||||||
|
|
||||||
v := map[string]any{}
|
|
||||||
|
|
||||||
err := codec.Decode([]byte(original), v)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
assert.Equal(t, decoded, v)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("InvalidData", func(t *testing.T) {
|
|
||||||
codec := Codec{}
|
|
||||||
|
|
||||||
v := map[string]any{}
|
|
||||||
|
|
||||||
err := codec.Decode([]byte(`invalid data`), v)
|
|
||||||
require.Error(t, err)
|
|
||||||
|
|
||||||
t.Logf("decoding failed as expected: %s", err)
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,74 +0,0 @@
|
||||||
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]any, path []string) map[string]any {
|
|
||||||
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]any)
|
|
||||||
m[k] = m3
|
|
||||||
m = m3
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
m3, ok := m2.(map[string]any)
|
|
||||||
if !ok {
|
|
||||||
// intermediate key is a value
|
|
||||||
// => replace with a new map
|
|
||||||
m3 = make(map[string]any)
|
|
||||||
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 the main package.
|
|
||||||
// TODO: move it to a common place.
|
|
||||||
func flattenAndMergeMap(shadow, m map[string]any, prefix, delimiter string) map[string]any {
|
|
||||||
if shadow != nil && prefix != "" && shadow[prefix] != nil {
|
|
||||||
// prefix is shadowed => nothing more to flatten
|
|
||||||
return shadow
|
|
||||||
}
|
|
||||||
if shadow == nil {
|
|
||||||
shadow = make(map[string]any)
|
|
||||||
}
|
|
||||||
|
|
||||||
var m2 map[string]any
|
|
||||||
if prefix != "" {
|
|
||||||
prefix += delimiter
|
|
||||||
}
|
|
||||||
for k, val := range m {
|
|
||||||
fullKey := prefix + k
|
|
||||||
switch val := val.(type) {
|
|
||||||
case map[string]any:
|
|
||||||
m2 = val
|
|
||||||
case map[any]any:
|
|
||||||
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
|
|
||||||
}
|
|
|
@ -1,86 +0,0 @@
|
||||||
package javaproperties
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/magiconair/properties"
|
|
||||||
"github.com/spf13/cast"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Codec implements the encoding.Encoder and encoding.Decoder interfaces for Java properties encoding.
|
|
||||||
type Codec struct {
|
|
||||||
KeyDelimiter string
|
|
||||||
|
|
||||||
// Store read properties on the object so that we can write back in order with comments.
|
|
||||||
// This will only be used if the configuration read is a properties file.
|
|
||||||
// TODO: drop this feature in v2
|
|
||||||
// TODO: make use of the global properties object optional
|
|
||||||
Properties *properties.Properties
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Codec) Encode(v map[string]any) ([]byte, error) {
|
|
||||||
if c.Properties == nil {
|
|
||||||
c.Properties = properties.NewProperties()
|
|
||||||
}
|
|
||||||
|
|
||||||
flattened := map[string]any{}
|
|
||||||
|
|
||||||
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 {
|
|
||||||
_, _, err := c.Properties.Set(key, cast.ToString(flattened[key]))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var buf bytes.Buffer
|
|
||||||
|
|
||||||
_, err := c.Properties.WriteComment(&buf, "#", properties.UTF8)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return buf.Bytes(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Codec) Decode(b []byte, v map[string]any) error {
|
|
||||||
var err error
|
|
||||||
c.Properties, err = properties.Load(b, properties.UTF8)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, key := range c.Properties.Keys() {
|
|
||||||
// ignore existence check: we know it's there
|
|
||||||
value, _ := c.Properties.Get(key)
|
|
||||||
|
|
||||||
// recursively build nested maps
|
|
||||||
path := strings.Split(key, c.keyDelimiter())
|
|
||||||
lastKey := strings.ToLower(path[len(path)-1])
|
|
||||||
deepestMap := deepSearch(v, path[0:len(path)-1])
|
|
||||||
|
|
||||||
// set innermost value
|
|
||||||
deepestMap[lastKey] = value
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c Codec) keyDelimiter() string {
|
|
||||||
if c.KeyDelimiter == "" {
|
|
||||||
return "."
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.KeyDelimiter
|
|
||||||
}
|
|
|
@ -1,75 +0,0 @@
|
||||||
package javaproperties
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
// original form of the data.
|
|
||||||
const original = `#key-value pair
|
|
||||||
key = value
|
|
||||||
map.key = value
|
|
||||||
`
|
|
||||||
|
|
||||||
// encoded form of the data.
|
|
||||||
const encoded = `key = value
|
|
||||||
map.key = value
|
|
||||||
`
|
|
||||||
|
|
||||||
// data is Viper's internal representation.
|
|
||||||
var data = map[string]any{
|
|
||||||
"key": "value",
|
|
||||||
"map": map[string]any{
|
|
||||||
"key": "value",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCodec_Encode(t *testing.T) {
|
|
||||||
codec := Codec{}
|
|
||||||
|
|
||||||
b, err := codec.Encode(data)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
assert.Equal(t, encoded, string(b))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCodec_Decode(t *testing.T) {
|
|
||||||
t.Run("OK", func(t *testing.T) {
|
|
||||||
codec := Codec{}
|
|
||||||
|
|
||||||
v := map[string]any{}
|
|
||||||
|
|
||||||
err := codec.Decode([]byte(original), v)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
assert.Equal(t, data, v)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("InvalidData", func(t *testing.T) {
|
|
||||||
t.Skip("TODO: needs invalid data example")
|
|
||||||
|
|
||||||
codec := Codec{}
|
|
||||||
|
|
||||||
v := map[string]any{}
|
|
||||||
|
|
||||||
codec.Decode([]byte(``), v)
|
|
||||||
|
|
||||||
assert.Empty(t, v)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCodec_DecodeEncode(t *testing.T) {
|
|
||||||
codec := Codec{}
|
|
||||||
|
|
||||||
v := map[string]any{}
|
|
||||||
|
|
||||||
err := codec.Decode([]byte(original), v)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
b, err := codec.Encode(data)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
assert.Equal(t, original, string(b))
|
|
||||||
}
|
|
|
@ -1,74 +0,0 @@
|
||||||
package javaproperties
|
|
||||||
|
|
||||||
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]any, path []string) map[string]any {
|
|
||||||
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]any)
|
|
||||||
m[k] = m3
|
|
||||||
m = m3
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
m3, ok := m2.(map[string]any)
|
|
||||||
if !ok {
|
|
||||||
// intermediate key is a value
|
|
||||||
// => replace with a new map
|
|
||||||
m3 = make(map[string]any)
|
|
||||||
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 the main package.
|
|
||||||
// TODO: move it to a common place.
|
|
||||||
func flattenAndMergeMap(shadow, m map[string]any, prefix, delimiter string) map[string]any {
|
|
||||||
if shadow != nil && prefix != "" && shadow[prefix] != nil {
|
|
||||||
// prefix is shadowed => nothing more to flatten
|
|
||||||
return shadow
|
|
||||||
}
|
|
||||||
if shadow == nil {
|
|
||||||
shadow = make(map[string]any)
|
|
||||||
}
|
|
||||||
|
|
||||||
var m2 map[string]any
|
|
||||||
if prefix != "" {
|
|
||||||
prefix += delimiter
|
|
||||||
}
|
|
||||||
for k, val := range m {
|
|
||||||
fullKey := prefix + k
|
|
||||||
switch val := val.(type) {
|
|
||||||
case map[string]any:
|
|
||||||
m2 = val
|
|
||||||
case map[any]any:
|
|
||||||
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
|
|
||||||
}
|
|
|
@ -1,16 +0,0 @@
|
||||||
package toml
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/pelletier/go-toml/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Codec implements the encoding.Encoder and encoding.Decoder interfaces for TOML encoding.
|
|
||||||
type Codec struct{}
|
|
||||||
|
|
||||||
func (Codec) Encode(v map[string]any) ([]byte, error) {
|
|
||||||
return toml.Marshal(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (Codec) Decode(b []byte, v map[string]any) error {
|
|
||||||
return toml.Unmarshal(b, &v)
|
|
||||||
}
|
|
|
@ -1,97 +0,0 @@
|
||||||
package toml
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
// original form of the data.
|
|
||||||
const original = `# key-value pair
|
|
||||||
key = "value"
|
|
||||||
list = ["item1", "item2", "item3"]
|
|
||||||
|
|
||||||
[map]
|
|
||||||
key = "value"
|
|
||||||
|
|
||||||
# nested
|
|
||||||
# map
|
|
||||||
[nested_map]
|
|
||||||
[nested_map.map]
|
|
||||||
key = "value"
|
|
||||||
list = [
|
|
||||||
"item1",
|
|
||||||
"item2",
|
|
||||||
"item3",
|
|
||||||
]
|
|
||||||
`
|
|
||||||
|
|
||||||
// encoded form of the data.
|
|
||||||
const encoded = `key = 'value'
|
|
||||||
list = ['item1', 'item2', 'item3']
|
|
||||||
|
|
||||||
[map]
|
|
||||||
key = 'value'
|
|
||||||
|
|
||||||
[nested_map]
|
|
||||||
[nested_map.map]
|
|
||||||
key = 'value'
|
|
||||||
list = ['item1', 'item2', 'item3']
|
|
||||||
`
|
|
||||||
|
|
||||||
// data is Viper's internal representation.
|
|
||||||
var data = map[string]any{
|
|
||||||
"key": "value",
|
|
||||||
"list": []any{
|
|
||||||
"item1",
|
|
||||||
"item2",
|
|
||||||
"item3",
|
|
||||||
},
|
|
||||||
"map": map[string]any{
|
|
||||||
"key": "value",
|
|
||||||
},
|
|
||||||
"nested_map": map[string]any{
|
|
||||||
"map": map[string]any{
|
|
||||||
"key": "value",
|
|
||||||
"list": []any{
|
|
||||||
"item1",
|
|
||||||
"item2",
|
|
||||||
"item3",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCodec_Encode(t *testing.T) {
|
|
||||||
codec := Codec{}
|
|
||||||
|
|
||||||
b, err := codec.Encode(data)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
assert.Equal(t, encoded, string(b))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCodec_Decode(t *testing.T) {
|
|
||||||
t.Run("OK", func(t *testing.T) {
|
|
||||||
codec := Codec{}
|
|
||||||
|
|
||||||
v := map[string]any{}
|
|
||||||
|
|
||||||
err := codec.Decode([]byte(original), v)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
assert.Equal(t, data, v)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("InvalidData", func(t *testing.T) {
|
|
||||||
codec := Codec{}
|
|
||||||
|
|
||||||
v := map[string]any{}
|
|
||||||
|
|
||||||
err := codec.Decode([]byte(`invalid data`), v)
|
|
||||||
require.Error(t, err)
|
|
||||||
|
|
||||||
t.Logf("decoding failed as expected: %s", err)
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,14 +0,0 @@
|
||||||
package yaml
|
|
||||||
|
|
||||||
import "gopkg.in/yaml.v3"
|
|
||||||
|
|
||||||
// Codec implements the encoding.Encoder and encoding.Decoder interfaces for YAML encoding.
|
|
||||||
type Codec struct{}
|
|
||||||
|
|
||||||
func (Codec) Encode(v map[string]any) ([]byte, error) {
|
|
||||||
return yaml.Marshal(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (Codec) Decode(b []byte, v map[string]any) error {
|
|
||||||
return yaml.Unmarshal(b, &v)
|
|
||||||
}
|
|
|
@ -1,128 +0,0 @@
|
||||||
package yaml
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
// original form of the data.
|
|
||||||
const original = `# key-value pair
|
|
||||||
key: value
|
|
||||||
list:
|
|
||||||
- item1
|
|
||||||
- item2
|
|
||||||
- item3
|
|
||||||
map:
|
|
||||||
key: value
|
|
||||||
|
|
||||||
# nested
|
|
||||||
# map
|
|
||||||
nested_map:
|
|
||||||
map:
|
|
||||||
key: value
|
|
||||||
list:
|
|
||||||
- item1
|
|
||||||
- item2
|
|
||||||
- item3
|
|
||||||
`
|
|
||||||
|
|
||||||
// encoded form of the data.
|
|
||||||
const encoded = `key: value
|
|
||||||
list:
|
|
||||||
- item1
|
|
||||||
- item2
|
|
||||||
- item3
|
|
||||||
map:
|
|
||||||
key: value
|
|
||||||
nested_map:
|
|
||||||
map:
|
|
||||||
key: value
|
|
||||||
list:
|
|
||||||
- item1
|
|
||||||
- item2
|
|
||||||
- item3
|
|
||||||
`
|
|
||||||
|
|
||||||
// decoded form of the data.
|
|
||||||
//
|
|
||||||
// In case of YAML it's slightly different from Viper's internal representation
|
|
||||||
// (e.g. map is decoded into a map with interface key).
|
|
||||||
var decoded = map[string]any{
|
|
||||||
"key": "value",
|
|
||||||
"list": []any{
|
|
||||||
"item1",
|
|
||||||
"item2",
|
|
||||||
"item3",
|
|
||||||
},
|
|
||||||
"map": map[string]any{
|
|
||||||
"key": "value",
|
|
||||||
},
|
|
||||||
"nested_map": map[string]any{
|
|
||||||
"map": map[string]any{
|
|
||||||
"key": "value",
|
|
||||||
"list": []any{
|
|
||||||
"item1",
|
|
||||||
"item2",
|
|
||||||
"item3",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// data is Viper's internal representation.
|
|
||||||
var data = map[string]any{
|
|
||||||
"key": "value",
|
|
||||||
"list": []any{
|
|
||||||
"item1",
|
|
||||||
"item2",
|
|
||||||
"item3",
|
|
||||||
},
|
|
||||||
"map": map[string]any{
|
|
||||||
"key": "value",
|
|
||||||
},
|
|
||||||
"nested_map": map[string]any{
|
|
||||||
"map": map[string]any{
|
|
||||||
"key": "value",
|
|
||||||
"list": []any{
|
|
||||||
"item1",
|
|
||||||
"item2",
|
|
||||||
"item3",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCodec_Encode(t *testing.T) {
|
|
||||||
codec := Codec{}
|
|
||||||
|
|
||||||
b, err := codec.Encode(data)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
assert.Equal(t, encoded, string(b))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCodec_Decode(t *testing.T) {
|
|
||||||
t.Run("OK", func(t *testing.T) {
|
|
||||||
codec := Codec{}
|
|
||||||
|
|
||||||
v := map[string]any{}
|
|
||||||
|
|
||||||
err := codec.Decode([]byte(original), v)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
assert.Equal(t, decoded, v)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("InvalidData", func(t *testing.T) {
|
|
||||||
codec := Codec{}
|
|
||||||
|
|
||||||
v := map[string]any{}
|
|
||||||
|
|
||||||
err := codec.Decode([]byte(`invalid data`), v)
|
|
||||||
require.Error(t, err)
|
|
||||||
|
|
||||||
t.Logf("decoding failed as expected: %s", err)
|
|
||||||
})
|
|
||||||
}
|
|
Loading…
Reference in a new issue