Repository: zeromicro/go-zero
Pull request #2651 (2022-11-30 14:42:27)

feat: accept camelcase for config keys


Patch:
diff --git a/core/conf/config.go b/core/conf/config.go
index f9532e90..605f218c 100644
--- a/core/conf/config.go
+++ b/core/conf/config.go
@@ -7,9 +7,13 @@ import (
 	"path"
 	"strings"
 
+	"github.com/zeromicro/go-zero/core/jsonx"
 	"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{
 	".json": LoadFromJsonBytes,
 	".toml": LoadFromTomlBytes,
@@ -49,7 +53,12 @@ func LoadConfig(file string, v interface{}, opts ...Option) error {
 
 // LoadFromJsonBytes loads config into v from content json bytes.
 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.
@@ -60,12 +69,22 @@ func LoadConfigFromJsonBytes(content []byte, v interface{}) error {
 
 // LoadFromTomlBytes loads config into v from content toml bytes.
 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.
 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.
@@ -80,3 +99,66 @@ func MustLoad(path string, v interface{}, opts ...Option) {
 		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
+}
diff --git a/core/mapping/jsonunmarshaler.go b/core/mapping/jsonunmarshaler.go
index ce698830..540581a3 100644
--- a/core/mapping/jsonunmarshaler.go
+++ b/core/mapping/jsonunmarshaler.go
@@ -11,18 +11,26 @@ const jsonTagKey = "json"
 var jsonUnmarshaler = NewUnmarshaler(jsonTagKey)
 
 // UnmarshalJsonBytes unmarshals content into v.
-func UnmarshalJsonBytes(content []byte, v interface{}) error {
-	return unmarshalJsonBytes(content, v, jsonUnmarshaler)
+func UnmarshalJsonBytes(content []byte, v interface{}, opts ...UnmarshalOption) error {
+	return unmarshalJsonBytes(content, v, getJsonUnmarshaler(opts...))
 }
 
 // UnmarshalJsonMap unmarshals content from m into v.
-func UnmarshalJsonMap(m map[string]interface{}, v interface{}) error {
-	return jsonUnmarshaler.Unmarshal(m, v)
+func UnmarshalJsonMap(m map[string]interface{}, v interface{}, opts ...UnmarshalOption) error {
+	return getJsonUnmarshaler(opts...).Unmarshal(m, v)
 }
 
 // UnmarshalJsonReader unmarshals content from reader into v.
-func UnmarshalJsonReader(reader io.Reader, v interface{}) error {
-	return unmarshalJsonReader(reader, v, jsonUnmarshaler)
+func UnmarshalJsonReader(reader io.Reader, v interface{}, opts ...UnmarshalOption) error {
+	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 {
diff --git a/core/mapping/tomlunmarshaler.go b/core/mapping/tomlunmarshaler.go
index 1d5e1ad1..ccf7731a 100644
--- a/core/mapping/tomlunmarshaler.go
+++ b/core/mapping/tomlunmarshaler.go
@@ -1,29 +1,27 @@
 package mapping
 
 import (
-	"bytes"
-	"encoding/json"
 	"io"
 
-	"github.com/pelletier/go-toml/v2"
+	"github.com/zeromicro/go-zero/internal/encoding"
 )
 
 // UnmarshalTomlBytes unmarshals TOML bytes into the given v.
-func UnmarshalTomlBytes(content []byte, v interface{}) error {
-	return UnmarshalTomlReader(bytes.NewReader(content), v)
-}
-
-// UnmarshalTomlReader unmarshals TOML from the given io.Reader into the given v.
-func UnmarshalTomlReader(r io.Reader, v interface{}) error {
-	var val interface{}
-	if err := toml.NewDecoder(r).Decode(&val); err != nil {
+func UnmarshalTomlBytes(content []byte, v interface{}, opts ...UnmarshalOption) error {
+	b, err := encoding.TomlToJson(content)
+	if err != nil {
 		return err
 	}
 
-	var buf bytes.Buffer
-	if err := json.NewEncoder(&buf).Encode(val); err != nil {
+	return UnmarshalJsonBytes(b, v, opts...)
+}
+
+// UnmarshalTomlReader unmarshals TOML from the given io.Reader into the given v.
+func UnmarshalTomlReader(r io.Reader, v interface{}, opts ...UnmarshalOption) error {
+	b, err := io.ReadAll(r)
+	if err != nil {
 		return err
 	}
 
-	return UnmarshalJsonReader(&buf, v)
+	return UnmarshalTomlBytes(b, v, opts...)
 }
diff --git a/core/mapping/yamlunmarshaler.go b/core/mapping/yamlunmarshaler.go
index 9ae89214..ad354cdb 100644
--- a/core/mapping/yamlunmarshaler.go
+++ b/core/mapping/yamlunmarshaler.go
@@ -1,101 +1,27 @@
 package mapping
 
 import (
-	"encoding/json"
-	"errors"
 	"io"
 
-	"gopkg.in/yaml.v2"
-)
-
-// 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)
+	"github.com/zeromicro/go-zero/internal/encoding"
 )
 
 // UnmarshalYamlBytes unmarshals content into v.
-func UnmarshalYamlBytes(content []byte, v interface{}) error {
-	return unmarshalYamlBytes(content, v, yamlUnmarshaler)
-}
-
-// UnmarshalYamlReader unmarshals content from reader into v.
-func UnmarshalYamlReader(reader io.Reader, v interface{}) error {
-	return unmarshalYamlReader(reader, v, yamlUnmarshaler)
-}
-
-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 {
+func UnmarshalYamlBytes(content []byte, v interface{}, opts ...UnmarshalOption) error {
+	b, err := encoding.YamlToJson(content)
+	if err != nil {
 		return err
 	}
 
-	return unmarshal(unmarshaler, o, v)
+	return UnmarshalJsonBytes(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 {
+// UnmarshalYamlReader unmarshals content from reader into v.
+func UnmarshalYamlReader(reader io.Reader, v interface{}, opts ...UnmarshalOption) error {
+	b, err := io.ReadAll(reader)
+	if err != nil {
 		return err
 	}
 
-	*out.(*interface{}) = cleanupMapValue(res)
-	return nil
+	return UnmarshalYamlBytes(b, v, opts...)
 }
diff --git a/internal/encoding/encoding.go b/internal/encoding/encoding.go
new file mode 100644
index 00000000..cfa754d9
--- /dev/null
+++ b/internal/encoding/encoding.go
@@ -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)
+	}
+}


Test patch:
diff --git a/core/conf/config_test.go b/core/conf/config_test.go
index b0f5077b..6485ef46 100644
--- a/core/conf/config_test.go
+++ b/core/conf/config_test.go
@@ -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) {
 	text := `a = "foo"
 b = 1
@@ -81,6 +97,65 @@ d = "abcd!@#$112"
 	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) {
 	text := `a = "foo"
 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) {
 	tmpfile, err := os.CreateTemp(os.TempDir(), hash.Md5Hex([]byte(text))+"*"+ext)
 	if err != nil {
diff --git a/core/mapping/jsonunmarshaler_test.go b/core/mapping/jsonunmarshaler_test.go
index ccaf6b6c..28615d31 100644
--- a/core/mapping/jsonunmarshaler_test.go
+++ b/core/mapping/jsonunmarshaler_test.go
@@ -900,7 +900,9 @@ func TestUnmarshalMap(t *testing.T) {
 			Any string `json:",optional"`
 		}
 
-		err := UnmarshalJsonMap(m, &v)
+		err := UnmarshalJsonMap(m, &v, WithCanonicalKeyFunc(func(s string) string {
+			return s
+		}))
 		assert.Nil(t, err)
 		assert.True(t, len(v.Any) == 0)
 	})
diff --git a/core/mapping/tomlunmarshaler_test.go b/core/mapping/tomlunmarshaler_test.go
index 776bd345..aa847b6f 100644
--- a/core/mapping/tomlunmarshaler_test.go
+++ b/core/mapping/tomlunmarshaler_test.go
@@ -1,6 +1,7 @@
 package mapping
 
 import (
+	"strings"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
@@ -18,7 +19,7 @@ d = "abcd!@#$112"
 		C string `json:"c"`
 		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, 1, val.B)
 	assert.Equal(t, "${FOO}", val.C)
@@ -37,5 +38,12 @@ d = "abcd!@#$112"
 		C string `json:"c"`
 		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))
 }
diff --git a/core/mapping/yamlunmarshaler_test.go b/core/mapping/yamlunmarshaler_test.go
index 3a4d5066..eef849ac 100644
--- a/core/mapping/yamlunmarshaler_test.go
+++ b/core/mapping/yamlunmarshaler_test.go
@@ -934,9 +934,8 @@ func TestUnmarshalYamlReaderError(t *testing.T) {
 	err := UnmarshalYamlReader(reader, &v)
 	assert.NotNil(t, err)
 
-	reader = strings.NewReader("chenquan")
-	err = UnmarshalYamlReader(reader, &v)
-	assert.ErrorIs(t, err, ErrUnsupportedType)
+	reader = strings.NewReader("foo")
+	assert.Error(t, UnmarshalYamlReader(reader, &v))
 }
 
 func TestUnmarshalYamlBadReader(t *testing.T) {
@@ -1012,6 +1011,13 @@ func TestUnmarshalYamlMapRune(t *testing.T) {
 	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{}
 
 func (b *badReader) Read(_ []byte) (n int, err error) {
diff --git a/internal/encoding/encoding_test.go b/internal/encoding/encoding_test.go
new file mode 100644
index 00000000..b7f8b858
--- /dev/null
+++ b/internal/encoding/encoding_test.go
@@ -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))
+}