From dcfc9b79f16b649dbf8cb4caf804d670da2d188a Mon Sep 17 00:00:00 2001 From: Kevin Wan Date: Thu, 8 Dec 2022 22:01:36 +0800 Subject: [PATCH] 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 --- core/conf/config.go | 88 ++++++++++++- core/conf/config_test.go | 185 +++++++++++++++++++++++++++ core/mapping/jsonunmarshaler.go | 20 ++- core/mapping/jsonunmarshaler_test.go | 4 +- core/mapping/tomlunmarshaler.go | 26 ++-- core/mapping/tomlunmarshaler_test.go | 12 +- core/mapping/yamlunmarshaler.go | 94 ++------------ core/mapping/yamlunmarshaler_test.go | 12 +- internal/encoding/encoding.go | 75 +++++++++++ internal/encoding/encoding_test.go | 118 +++++++++++++++++ 10 files changed, 521 insertions(+), 113 deletions(-) create mode 100644 internal/encoding/encoding.go create mode 100644 internal/encoding/encoding_test.go diff --git a/core/conf/config.go b/core/conf/config.go index f9532e908871..605f218c44a5 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/conf/config_test.go b/core/conf/config_test.go index b0f5077bd4a2..6485ef464760 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.go b/core/mapping/jsonunmarshaler.go index ce69883088d9..540581a30344 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/jsonunmarshaler_test.go b/core/mapping/jsonunmarshaler_test.go index ccaf6b6c354c..28615d31b04e 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.go b/core/mapping/tomlunmarshaler.go index 1d5e1ad15b1c..ccf7731a13d3 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/tomlunmarshaler_test.go b/core/mapping/tomlunmarshaler_test.go index 776bd345e6fd..aa847b6fd5e6 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.go b/core/mapping/yamlunmarshaler.go index 9ae892146a7e..ad354cdb5b56 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/core/mapping/yamlunmarshaler_test.go b/core/mapping/yamlunmarshaler_test.go index 3a4d50667dc6..eef849ac8bd2 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.go b/internal/encoding/encoding.go new file mode 100644 index 000000000000..cfa754d95085 --- /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) + } +} diff --git a/internal/encoding/encoding_test.go b/internal/encoding/encoding_test.go new file mode 100644 index 000000000000..b7f8b858d2fb --- /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)) +}