feat: accept camelcase for config keys (#2651)
* feat: accept camelcase for config keys * chore: refactor * chore: refactor * chore: add more tests * chore: refactor * fix: map elements of array
This commit is contained in:
@@ -7,9 +7,13 @@ import (
|
|||||||
"path"
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/core/jsonx"
|
||||||
"github.com/zeromicro/go-zero/core/mapping"
|
"github.com/zeromicro/go-zero/core/mapping"
|
||||||
|
"github.com/zeromicro/go-zero/internal/encoding"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const distanceBetweenUpperAndLower = 32
|
||||||
|
|
||||||
var loaders = map[string]func([]byte, interface{}) error{
|
var loaders = map[string]func([]byte, interface{}) error{
|
||||||
".json": LoadFromJsonBytes,
|
".json": LoadFromJsonBytes,
|
||||||
".toml": LoadFromTomlBytes,
|
".toml": LoadFromTomlBytes,
|
||||||
@@ -49,7 +53,12 @@ func LoadConfig(file string, v interface{}, opts ...Option) error {
|
|||||||
|
|
||||||
// LoadFromJsonBytes loads config into v from content json bytes.
|
// LoadFromJsonBytes loads config into v from content json bytes.
|
||||||
func LoadFromJsonBytes(content []byte, v interface{}) error {
|
func LoadFromJsonBytes(content []byte, v interface{}) error {
|
||||||
return mapping.UnmarshalJsonBytes(content, v)
|
var m map[string]interface{}
|
||||||
|
if err := jsonx.Unmarshal(content, &m); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return mapping.UnmarshalJsonMap(toCamelCaseKeyMap(m), v, mapping.WithCanonicalKeyFunc(toCamelCase))
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadConfigFromJsonBytes loads config into v from content json bytes.
|
// LoadConfigFromJsonBytes loads config into v from content json bytes.
|
||||||
@@ -60,12 +69,22 @@ func LoadConfigFromJsonBytes(content []byte, v interface{}) error {
|
|||||||
|
|
||||||
// LoadFromTomlBytes loads config into v from content toml bytes.
|
// LoadFromTomlBytes loads config into v from content toml bytes.
|
||||||
func LoadFromTomlBytes(content []byte, v interface{}) error {
|
func LoadFromTomlBytes(content []byte, v interface{}) error {
|
||||||
return mapping.UnmarshalTomlBytes(content, v)
|
b, err := encoding.TomlToJson(content)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return LoadFromJsonBytes(b, v)
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadFromYamlBytes loads config into v from content yaml bytes.
|
// LoadFromYamlBytes loads config into v from content yaml bytes.
|
||||||
func LoadFromYamlBytes(content []byte, v interface{}) error {
|
func LoadFromYamlBytes(content []byte, v interface{}) error {
|
||||||
return mapping.UnmarshalYamlBytes(content, v)
|
b, err := encoding.YamlToJson(content)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return LoadFromJsonBytes(b, v)
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadConfigFromYamlBytes loads config into v from content yaml bytes.
|
// LoadConfigFromYamlBytes loads config into v from content yaml bytes.
|
||||||
@@ -80,3 +99,66 @@ func MustLoad(path string, v interface{}, opts ...Option) {
|
|||||||
log.Fatalf("error: config file %s, %s", path, err.Error())
|
log.Fatalf("error: config file %s, %s", path, err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func toCamelCase(s string) string {
|
||||||
|
var buf strings.Builder
|
||||||
|
buf.Grow(len(s))
|
||||||
|
var capNext bool
|
||||||
|
boundary := true
|
||||||
|
for _, v := range s {
|
||||||
|
isCap := v >= 'A' && v <= 'Z'
|
||||||
|
isLow := v >= 'a' && v <= 'z'
|
||||||
|
if boundary && (isCap || isLow) {
|
||||||
|
if capNext {
|
||||||
|
if isLow {
|
||||||
|
v -= distanceBetweenUpperAndLower
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if isCap {
|
||||||
|
v += distanceBetweenUpperAndLower
|
||||||
|
}
|
||||||
|
}
|
||||||
|
boundary = false
|
||||||
|
}
|
||||||
|
if isCap || isLow {
|
||||||
|
buf.WriteRune(v)
|
||||||
|
capNext = false
|
||||||
|
} else if v == ' ' || v == '\t' {
|
||||||
|
buf.WriteRune(v)
|
||||||
|
capNext = false
|
||||||
|
boundary = true
|
||||||
|
} else if v == '_' {
|
||||||
|
capNext = true
|
||||||
|
boundary = true
|
||||||
|
} else {
|
||||||
|
buf.WriteRune(v)
|
||||||
|
capNext = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func toCamelCaseInterface(v interface{}) interface{} {
|
||||||
|
switch vv := v.(type) {
|
||||||
|
case map[string]interface{}:
|
||||||
|
return toCamelCaseKeyMap(vv)
|
||||||
|
case []interface{}:
|
||||||
|
var arr []interface{}
|
||||||
|
for _, vvv := range vv {
|
||||||
|
arr = append(arr, toCamelCaseInterface(vvv))
|
||||||
|
}
|
||||||
|
return arr
|
||||||
|
default:
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toCamelCaseKeyMap(m map[string]interface{}) map[string]interface{} {
|
||||||
|
res := make(map[string]interface{})
|
||||||
|
for k, v := range m {
|
||||||
|
res[toCamelCase(k)] = toCamelCaseInterface(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|||||||
@@ -56,6 +56,22 @@ func TestConfigJson(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLoadFromJsonBytesArray(t *testing.T) {
|
||||||
|
input := []byte(`{"users": [{"name": "foo"}, {"Name": "bar"}]}`)
|
||||||
|
var val struct {
|
||||||
|
Users []struct {
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.NoError(t, LoadFromJsonBytes(input, &val))
|
||||||
|
var expect []string
|
||||||
|
for _, user := range val.Users {
|
||||||
|
expect = append(expect, user.Name)
|
||||||
|
}
|
||||||
|
assert.EqualValues(t, []string{"foo", "bar"}, expect)
|
||||||
|
}
|
||||||
|
|
||||||
func TestConfigToml(t *testing.T) {
|
func TestConfigToml(t *testing.T) {
|
||||||
text := `a = "foo"
|
text := `a = "foo"
|
||||||
b = 1
|
b = 1
|
||||||
@@ -81,6 +97,65 @@ d = "abcd!@#$112"
|
|||||||
assert.Equal(t, "abcd!@#$112", val.D)
|
assert.Equal(t, "abcd!@#$112", val.D)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestConfigJsonCanonical(t *testing.T) {
|
||||||
|
text := []byte(`{"a": "foo", "B": "bar"}`)
|
||||||
|
|
||||||
|
var val1 struct {
|
||||||
|
A string `json:"a"`
|
||||||
|
B string `json:"b"`
|
||||||
|
}
|
||||||
|
var val2 struct {
|
||||||
|
A string
|
||||||
|
B string
|
||||||
|
}
|
||||||
|
assert.NoError(t, LoadFromJsonBytes(text, &val1))
|
||||||
|
assert.Equal(t, "foo", val1.A)
|
||||||
|
assert.Equal(t, "bar", val1.B)
|
||||||
|
assert.NoError(t, LoadFromJsonBytes(text, &val2))
|
||||||
|
assert.Equal(t, "foo", val2.A)
|
||||||
|
assert.Equal(t, "bar", val2.B)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigTomlCanonical(t *testing.T) {
|
||||||
|
text := []byte(`a = "foo"
|
||||||
|
B = "bar"`)
|
||||||
|
|
||||||
|
var val1 struct {
|
||||||
|
A string `json:"a"`
|
||||||
|
B string `json:"b"`
|
||||||
|
}
|
||||||
|
var val2 struct {
|
||||||
|
A string
|
||||||
|
B string
|
||||||
|
}
|
||||||
|
assert.NoError(t, LoadFromTomlBytes(text, &val1))
|
||||||
|
assert.Equal(t, "foo", val1.A)
|
||||||
|
assert.Equal(t, "bar", val1.B)
|
||||||
|
assert.NoError(t, LoadFromTomlBytes(text, &val2))
|
||||||
|
assert.Equal(t, "foo", val2.A)
|
||||||
|
assert.Equal(t, "bar", val2.B)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigYamlCanonical(t *testing.T) {
|
||||||
|
text := []byte(`a: foo
|
||||||
|
B: bar`)
|
||||||
|
|
||||||
|
var val1 struct {
|
||||||
|
A string `json:"a"`
|
||||||
|
B string `json:"b"`
|
||||||
|
}
|
||||||
|
var val2 struct {
|
||||||
|
A string
|
||||||
|
B string
|
||||||
|
}
|
||||||
|
assert.NoError(t, LoadFromYamlBytes(text, &val1))
|
||||||
|
assert.Equal(t, "foo", val1.A)
|
||||||
|
assert.Equal(t, "bar", val1.B)
|
||||||
|
assert.NoError(t, LoadFromYamlBytes(text, &val2))
|
||||||
|
assert.Equal(t, "foo", val2.A)
|
||||||
|
assert.Equal(t, "bar", val2.B)
|
||||||
|
}
|
||||||
|
|
||||||
func TestConfigTomlEnv(t *testing.T) {
|
func TestConfigTomlEnv(t *testing.T) {
|
||||||
text := `a = "foo"
|
text := `a = "foo"
|
||||||
b = 1
|
b = 1
|
||||||
@@ -143,6 +218,116 @@ func TestConfigJsonEnv(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestToCamelCase(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
input string
|
||||||
|
expect string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
input: "",
|
||||||
|
expect: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "A",
|
||||||
|
expect: "a",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "a",
|
||||||
|
expect: "a",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "hello_world",
|
||||||
|
expect: "helloWorld",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "Hello_world",
|
||||||
|
expect: "helloWorld",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "hello_World",
|
||||||
|
expect: "helloWorld",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "helloWorld",
|
||||||
|
expect: "helloWorld",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "HelloWorld",
|
||||||
|
expect: "helloWorld",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "hello World",
|
||||||
|
expect: "hello world",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "Hello World",
|
||||||
|
expect: "hello world",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "Hello World",
|
||||||
|
expect: "hello world",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "Hello World foo_bar",
|
||||||
|
expect: "hello world fooBar",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "Hello World foo_Bar",
|
||||||
|
expect: "hello world fooBar",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "Hello World Foo_bar",
|
||||||
|
expect: "hello world fooBar",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "Hello World Foo_Bar",
|
||||||
|
expect: "hello world fooBar",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "你好 World Foo_Bar",
|
||||||
|
expect: "你好 world fooBar",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
test := test
|
||||||
|
t.Run(test.input, func(t *testing.T) {
|
||||||
|
assert.Equal(t, test.expect, toCamelCase(test.input))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadFromJsonBytesError(t *testing.T) {
|
||||||
|
var val struct{}
|
||||||
|
assert.Error(t, LoadFromJsonBytes([]byte(`hello`), &val))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadFromTomlBytesError(t *testing.T) {
|
||||||
|
var val struct{}
|
||||||
|
assert.Error(t, LoadFromTomlBytes([]byte(`hello`), &val))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadFromYamlBytesError(t *testing.T) {
|
||||||
|
var val struct{}
|
||||||
|
assert.Error(t, LoadFromYamlBytes([]byte(`':hello`), &val))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadFromYamlBytes(t *testing.T) {
|
||||||
|
input := []byte(`layer1:
|
||||||
|
layer2:
|
||||||
|
layer3: foo`)
|
||||||
|
var val struct {
|
||||||
|
Layer1 struct {
|
||||||
|
Layer2 struct {
|
||||||
|
Layer3 string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.NoError(t, LoadFromYamlBytes(input, &val))
|
||||||
|
assert.Equal(t, "foo", val.Layer1.Layer2.Layer3)
|
||||||
|
}
|
||||||
|
|
||||||
func createTempFile(ext, text string) (string, error) {
|
func createTempFile(ext, text string) (string, error) {
|
||||||
tmpfile, err := os.CreateTemp(os.TempDir(), hash.Md5Hex([]byte(text))+"*"+ext)
|
tmpfile, err := os.CreateTemp(os.TempDir(), hash.Md5Hex([]byte(text))+"*"+ext)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -11,18 +11,26 @@ const jsonTagKey = "json"
|
|||||||
var jsonUnmarshaler = NewUnmarshaler(jsonTagKey)
|
var jsonUnmarshaler = NewUnmarshaler(jsonTagKey)
|
||||||
|
|
||||||
// UnmarshalJsonBytes unmarshals content into v.
|
// UnmarshalJsonBytes unmarshals content into v.
|
||||||
func UnmarshalJsonBytes(content []byte, v interface{}) error {
|
func UnmarshalJsonBytes(content []byte, v interface{}, opts ...UnmarshalOption) error {
|
||||||
return unmarshalJsonBytes(content, v, jsonUnmarshaler)
|
return unmarshalJsonBytes(content, v, getJsonUnmarshaler(opts...))
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnmarshalJsonMap unmarshals content from m into v.
|
// UnmarshalJsonMap unmarshals content from m into v.
|
||||||
func UnmarshalJsonMap(m map[string]interface{}, v interface{}) error {
|
func UnmarshalJsonMap(m map[string]interface{}, v interface{}, opts ...UnmarshalOption) error {
|
||||||
return jsonUnmarshaler.Unmarshal(m, v)
|
return getJsonUnmarshaler(opts...).Unmarshal(m, v)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnmarshalJsonReader unmarshals content from reader into v.
|
// UnmarshalJsonReader unmarshals content from reader into v.
|
||||||
func UnmarshalJsonReader(reader io.Reader, v interface{}) error {
|
func UnmarshalJsonReader(reader io.Reader, v interface{}, opts ...UnmarshalOption) error {
|
||||||
return unmarshalJsonReader(reader, v, jsonUnmarshaler)
|
return unmarshalJsonReader(reader, v, getJsonUnmarshaler(opts...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func getJsonUnmarshaler(opts ...UnmarshalOption) *Unmarshaler {
|
||||||
|
if len(opts) > 0 {
|
||||||
|
return NewUnmarshaler(jsonTagKey, opts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return jsonUnmarshaler
|
||||||
}
|
}
|
||||||
|
|
||||||
func unmarshalJsonBytes(content []byte, v interface{}, unmarshaler *Unmarshaler) error {
|
func unmarshalJsonBytes(content []byte, v interface{}, unmarshaler *Unmarshaler) error {
|
||||||
|
|||||||
@@ -900,7 +900,9 @@ func TestUnmarshalMap(t *testing.T) {
|
|||||||
Any string `json:",optional"`
|
Any string `json:",optional"`
|
||||||
}
|
}
|
||||||
|
|
||||||
err := UnmarshalJsonMap(m, &v)
|
err := UnmarshalJsonMap(m, &v, WithCanonicalKeyFunc(func(s string) string {
|
||||||
|
return s
|
||||||
|
}))
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
assert.True(t, len(v.Any) == 0)
|
assert.True(t, len(v.Any) == 0)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,29 +1,27 @@
|
|||||||
package mapping
|
package mapping
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
"github.com/pelletier/go-toml/v2"
|
"github.com/zeromicro/go-zero/internal/encoding"
|
||||||
)
|
)
|
||||||
|
|
||||||
// UnmarshalTomlBytes unmarshals TOML bytes into the given v.
|
// UnmarshalTomlBytes unmarshals TOML bytes into the given v.
|
||||||
func UnmarshalTomlBytes(content []byte, v interface{}) error {
|
func UnmarshalTomlBytes(content []byte, v interface{}, opts ...UnmarshalOption) error {
|
||||||
return UnmarshalTomlReader(bytes.NewReader(content), v)
|
b, err := encoding.TomlToJson(content)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return UnmarshalJsonBytes(b, v, opts...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnmarshalTomlReader unmarshals TOML from the given io.Reader into the given v.
|
// UnmarshalTomlReader unmarshals TOML from the given io.Reader into the given v.
|
||||||
func UnmarshalTomlReader(r io.Reader, v interface{}) error {
|
func UnmarshalTomlReader(r io.Reader, v interface{}, opts ...UnmarshalOption) error {
|
||||||
var val interface{}
|
b, err := io.ReadAll(r)
|
||||||
if err := toml.NewDecoder(r).Decode(&val); err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var buf bytes.Buffer
|
return UnmarshalTomlBytes(b, v, opts...)
|
||||||
if err := json.NewEncoder(&buf).Encode(val); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return UnmarshalJsonReader(&buf, v)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package mapping
|
package mapping
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@@ -18,7 +19,7 @@ d = "abcd!@#$112"
|
|||||||
C string `json:"c"`
|
C string `json:"c"`
|
||||||
D string `json:"d"`
|
D string `json:"d"`
|
||||||
}
|
}
|
||||||
assert.Nil(t, UnmarshalTomlBytes([]byte(input), &val))
|
assert.NoError(t, UnmarshalTomlReader(strings.NewReader(input), &val))
|
||||||
assert.Equal(t, "foo", val.A)
|
assert.Equal(t, "foo", val.A)
|
||||||
assert.Equal(t, 1, val.B)
|
assert.Equal(t, 1, val.B)
|
||||||
assert.Equal(t, "${FOO}", val.C)
|
assert.Equal(t, "${FOO}", val.C)
|
||||||
@@ -37,5 +38,12 @@ d = "abcd!@#$112"
|
|||||||
C string `json:"c"`
|
C string `json:"c"`
|
||||||
D string `json:"d"`
|
D string `json:"d"`
|
||||||
}
|
}
|
||||||
assert.NotNil(t, UnmarshalTomlBytes([]byte(input), &val))
|
assert.Error(t, UnmarshalTomlReader(strings.NewReader(input), &val))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnmarshalTomlBadReader(t *testing.T) {
|
||||||
|
var val struct {
|
||||||
|
A string `json:"a"`
|
||||||
|
}
|
||||||
|
assert.Error(t, UnmarshalTomlReader(new(badReader), &val))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,101 +1,27 @@
|
|||||||
package mapping
|
package mapping
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
"gopkg.in/yaml.v2"
|
"github.com/zeromicro/go-zero/internal/encoding"
|
||||||
)
|
|
||||||
|
|
||||||
// To make .json & .yaml consistent, we just use json as the tag key.
|
|
||||||
const yamlTagKey = "json"
|
|
||||||
|
|
||||||
var (
|
|
||||||
// ErrUnsupportedType is an error that indicates the config format is not supported.
|
|
||||||
ErrUnsupportedType = errors.New("only map-like configs are supported")
|
|
||||||
|
|
||||||
yamlUnmarshaler = NewUnmarshaler(yamlTagKey)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// UnmarshalYamlBytes unmarshals content into v.
|
// UnmarshalYamlBytes unmarshals content into v.
|
||||||
func UnmarshalYamlBytes(content []byte, v interface{}) error {
|
func UnmarshalYamlBytes(content []byte, v interface{}, opts ...UnmarshalOption) error {
|
||||||
return unmarshalYamlBytes(content, v, yamlUnmarshaler)
|
b, err := encoding.YamlToJson(content)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return UnmarshalJsonBytes(b, v, opts...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnmarshalYamlReader unmarshals content from reader into v.
|
// UnmarshalYamlReader unmarshals content from reader into v.
|
||||||
func UnmarshalYamlReader(reader io.Reader, v interface{}) error {
|
func UnmarshalYamlReader(reader io.Reader, v interface{}, opts ...UnmarshalOption) error {
|
||||||
return unmarshalYamlReader(reader, v, yamlUnmarshaler)
|
b, err := io.ReadAll(reader)
|
||||||
}
|
if err != nil {
|
||||||
|
|
||||||
func cleanupInterfaceMap(in map[interface{}]interface{}) map[string]interface{} {
|
|
||||||
res := make(map[string]interface{})
|
|
||||||
for k, v := range in {
|
|
||||||
res[Repr(k)] = cleanupMapValue(v)
|
|
||||||
}
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
func cleanupInterfaceNumber(in interface{}) json.Number {
|
|
||||||
return json.Number(Repr(in))
|
|
||||||
}
|
|
||||||
|
|
||||||
func cleanupInterfaceSlice(in []interface{}) []interface{} {
|
|
||||||
res := make([]interface{}, len(in))
|
|
||||||
for i, v := range in {
|
|
||||||
res[i] = cleanupMapValue(v)
|
|
||||||
}
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
func cleanupMapValue(v interface{}) interface{} {
|
|
||||||
switch v := v.(type) {
|
|
||||||
case []interface{}:
|
|
||||||
return cleanupInterfaceSlice(v)
|
|
||||||
case map[interface{}]interface{}:
|
|
||||||
return cleanupInterfaceMap(v)
|
|
||||||
case bool, string:
|
|
||||||
return v
|
|
||||||
case int, uint, int8, uint8, int16, uint16, int32, uint32, int64, uint64, float32, float64:
|
|
||||||
return cleanupInterfaceNumber(v)
|
|
||||||
default:
|
|
||||||
return Repr(v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func unmarshal(unmarshaler *Unmarshaler, o, v interface{}) error {
|
|
||||||
if m, ok := o.(map[string]interface{}); ok {
|
|
||||||
return unmarshaler.Unmarshal(m, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
return ErrUnsupportedType
|
|
||||||
}
|
|
||||||
|
|
||||||
func unmarshalYamlBytes(content []byte, v interface{}, unmarshaler *Unmarshaler) error {
|
|
||||||
var o interface{}
|
|
||||||
if err := yamlUnmarshal(content, &o); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return unmarshal(unmarshaler, o, v)
|
return UnmarshalYamlBytes(b, v, opts...)
|
||||||
}
|
|
||||||
|
|
||||||
func unmarshalYamlReader(reader io.Reader, v interface{}, unmarshaler *Unmarshaler) error {
|
|
||||||
var res interface{}
|
|
||||||
if err := yaml.NewDecoder(reader).Decode(&res); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return unmarshal(unmarshaler, cleanupMapValue(res), v)
|
|
||||||
}
|
|
||||||
|
|
||||||
// yamlUnmarshal YAML to map[string]interface{} instead of map[interface{}]interface{}.
|
|
||||||
func yamlUnmarshal(in []byte, out interface{}) error {
|
|
||||||
var res interface{}
|
|
||||||
if err := yaml.Unmarshal(in, &res); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
*out.(*interface{}) = cleanupMapValue(res)
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -934,9 +934,8 @@ func TestUnmarshalYamlReaderError(t *testing.T) {
|
|||||||
err := UnmarshalYamlReader(reader, &v)
|
err := UnmarshalYamlReader(reader, &v)
|
||||||
assert.NotNil(t, err)
|
assert.NotNil(t, err)
|
||||||
|
|
||||||
reader = strings.NewReader("chenquan")
|
reader = strings.NewReader("foo")
|
||||||
err = UnmarshalYamlReader(reader, &v)
|
assert.Error(t, UnmarshalYamlReader(reader, &v))
|
||||||
assert.ErrorIs(t, err, ErrUnsupportedType)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUnmarshalYamlBadReader(t *testing.T) {
|
func TestUnmarshalYamlBadReader(t *testing.T) {
|
||||||
@@ -1012,6 +1011,13 @@ func TestUnmarshalYamlMapRune(t *testing.T) {
|
|||||||
assert.Equal(t, rune(3), v.Machine["node3"])
|
assert.Equal(t, rune(3), v.Machine["node3"])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUnmarshalYamlBadInput(t *testing.T) {
|
||||||
|
var v struct {
|
||||||
|
Any string
|
||||||
|
}
|
||||||
|
assert.Error(t, UnmarshalYamlBytes([]byte("':foo"), &v))
|
||||||
|
}
|
||||||
|
|
||||||
type badReader struct{}
|
type badReader struct{}
|
||||||
|
|
||||||
func (b *badReader) Read(_ []byte) (n int, err error) {
|
func (b *badReader) Read(_ []byte) (n int, err error) {
|
||||||
|
|||||||
75
internal/encoding/encoding.go
Normal file
75
internal/encoding/encoding.go
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
package encoding
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
"github.com/pelletier/go-toml/v2"
|
||||||
|
"github.com/zeromicro/go-zero/core/lang"
|
||||||
|
"gopkg.in/yaml.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TomlToJson(data []byte) ([]byte, error) {
|
||||||
|
var val interface{}
|
||||||
|
if err := toml.NewDecoder(bytes.NewReader(data)).Decode(&val); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if err := json.NewEncoder(&buf).Encode(val); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func YamlToJson(data []byte) ([]byte, error) {
|
||||||
|
var val interface{}
|
||||||
|
if err := yaml.Unmarshal(data, &val); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
val = toStringKeyMap(val)
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if err := json.NewEncoder(&buf).Encode(val); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertKeyToString(in map[interface{}]interface{}) map[string]interface{} {
|
||||||
|
res := make(map[string]interface{})
|
||||||
|
for k, v := range in {
|
||||||
|
res[lang.Repr(k)] = toStringKeyMap(v)
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertNumberToJsonNumber(in interface{}) json.Number {
|
||||||
|
return json.Number(lang.Repr(in))
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertSlice(in []interface{}) []interface{} {
|
||||||
|
res := make([]interface{}, len(in))
|
||||||
|
for i, v := range in {
|
||||||
|
res[i] = toStringKeyMap(v)
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
func toStringKeyMap(v interface{}) interface{} {
|
||||||
|
switch v := v.(type) {
|
||||||
|
case []interface{}:
|
||||||
|
return convertSlice(v)
|
||||||
|
case map[interface{}]interface{}:
|
||||||
|
return convertKeyToString(v)
|
||||||
|
case bool, string:
|
||||||
|
return v
|
||||||
|
case int, uint, int8, uint8, int16, uint16, int32, uint32, int64, uint64, float32, float64:
|
||||||
|
return convertNumberToJsonNumber(v)
|
||||||
|
default:
|
||||||
|
return lang.Repr(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
118
internal/encoding/encoding_test.go
Normal file
118
internal/encoding/encoding_test.go
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
package encoding
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTomlToJson(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
input string
|
||||||
|
expect string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
input: "a = \"foo\"\nb = 1\nc = \"${FOO}\"\nd = \"abcd!@#$112\"",
|
||||||
|
expect: "{\"a\":\"foo\",\"b\":1,\"c\":\"${FOO}\",\"d\":\"abcd!@#$112\"}\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "a = \"foo\"\nb = 1\nc = \"${FOO}\"\nd = \"abcd!@#$112\"",
|
||||||
|
expect: "{\"a\":\"foo\",\"b\":1,\"c\":\"${FOO}\",\"d\":\"abcd!@#$112\"}\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "a = \"foo\"\nb = 1\nc = \"${FOO}\"\nd = \"abcd!@#$112\"",
|
||||||
|
expect: "{\"a\":\"foo\",\"b\":1,\"c\":\"${FOO}\",\"d\":\"abcd!@#$112\"}\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "a = \"foo\"\nb = 1\nc = \"${FOO}\"\nd = \"abcd!@#$112\"",
|
||||||
|
expect: "{\"a\":\"foo\",\"b\":1,\"c\":\"${FOO}\",\"d\":\"abcd!@#$112\"}\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "a = \"foo\"\nb = 1\nc = \"${FOO}\"\nd = \"abcd!@#$112\"",
|
||||||
|
expect: "{\"a\":\"foo\",\"b\":1,\"c\":\"${FOO}\",\"d\":\"abcd!@#$112\"}\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "a = \"foo\"\nb = 1\nc = \"${FOO}\"\nd = \"abcd!@#$112\"\n",
|
||||||
|
expect: "{\"a\":\"foo\",\"b\":1,\"c\":\"${FOO}\",\"d\":\"abcd!@#$112\"}\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "a = \"foo\"\nb = 1\nc = \"${FOO}\"\nd = \"abcd!@#$112\"\n",
|
||||||
|
expect: "{\"a\":\"foo\",\"b\":1,\"c\":\"${FOO}\",\"d\":\"abcd!@#$112\"}\n",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
test := test
|
||||||
|
t.Run(test.input, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
got, err := TomlToJson([]byte(test.input))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, test.expect, string(got))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTomlToJsonError(t *testing.T) {
|
||||||
|
_, err := TomlToJson([]byte("foo"))
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestYamlToJson(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
input string
|
||||||
|
expect string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
input: "a: foo\nb: 1\nc: ${FOO}\nd: abcd!@#$112",
|
||||||
|
expect: "{\"a\":\"foo\",\"b\":1,\"c\":\"${FOO}\",\"d\":\"abcd!@#$112\"}\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "a: foo\nb: 1\nc: ${FOO}\nd: abcd!@#$112",
|
||||||
|
expect: "{\"a\":\"foo\",\"b\":1,\"c\":\"${FOO}\",\"d\":\"abcd!@#$112\"}\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "a: foo\nb: 1\nc: ${FOO}\nd: abcd!@#$112",
|
||||||
|
expect: "{\"a\":\"foo\",\"b\":1,\"c\":\"${FOO}\",\"d\":\"abcd!@#$112\"}\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "a: foo\nb: 1\nc: ${FOO}\nd: abcd!@#$112",
|
||||||
|
expect: "{\"a\":\"foo\",\"b\":1,\"c\":\"${FOO}\",\"d\":\"abcd!@#$112\"}\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "a: foo\nb: 1\nc: ${FOO}\nd: abcd!@#$112",
|
||||||
|
expect: "{\"a\":\"foo\",\"b\":1,\"c\":\"${FOO}\",\"d\":\"abcd!@#$112\"}\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "a: foo\nb: 1\nc: ${FOO}\nd: abcd!@#$112\n",
|
||||||
|
expect: "{\"a\":\"foo\",\"b\":1,\"c\":\"${FOO}\",\"d\":\"abcd!@#$112\"}\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "a: foo\nb: 1\nc: ${FOO}\nd: abcd!@#$112\n",
|
||||||
|
expect: "{\"a\":\"foo\",\"b\":1,\"c\":\"${FOO}\",\"d\":\"abcd!@#$112\"}\n",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
test := test
|
||||||
|
t.Run(test.input, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
got, err := YamlToJson([]byte(test.input))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, test.expect, string(got))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestYamlToJsonError(t *testing.T) {
|
||||||
|
_, err := YamlToJson([]byte("':foo"))
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestYamlToJsonSlice(t *testing.T) {
|
||||||
|
b, err := YamlToJson([]byte(`foo:
|
||||||
|
- bar
|
||||||
|
- baz`))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, `{"foo":["bar","baz"]}
|
||||||
|
`, string(b))
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user