diff --git a/safeyaml/jsontoyaml.go b/safeyaml/jsontoyaml.go new file mode 100644 index 000000000..57b24bb78 --- /dev/null +++ b/safeyaml/jsontoyaml.go @@ -0,0 +1,85 @@ +package safeyaml + +import ( + "bytes" + "encoding/json" + "fmt" + + "gopkg.in/yaml.v2" +) + +var errClosingArrayDelim = fmt.Errorf("unexpected ']' delimiter") +var errClosingObjectDelim = fmt.Errorf("unexpected '}' delimiter") + +func JSONtoYAML(jsonBytes []byte) (interface{}, error) { + dec := json.NewDecoder(bytes.NewReader(jsonBytes)) + dec.UseNumber() + return tokenizerToYaml(dec) +} + +func tokenizerToYaml(dec *json.Decoder) (interface{}, error) { + tok, err := dec.Token() + if err != nil { + return nil, err + } + if tok == nil { + return nil, nil + } + switch v := tok.(type) { + case string: + return v, nil + case bool: + return v, nil + case float64: + return v, nil + case json.Number: + if numI, err := v.Int64(); err == nil { + return numI, nil + } + if numF, err := v.Float64(); err == nil { + return numF, nil + } + return v.String(), nil + case json.Delim: + switch v { + case '[': + arr := make([]interface{}, 0) + for { + elem, err := tokenizerToYaml(dec) + if err == errClosingArrayDelim { + break + } + if err != nil { + return nil, err + } + arr = append(arr, elem) + } + return arr, nil + case '{': + obj := make(yaml.MapSlice, 0) + for { + objectKeyI, err := tokenizerToYaml(dec) + if err == errClosingObjectDelim { + break + } + if err != nil { + return nil, err + } + objectValueI, err := tokenizerToYaml(dec) + if err != nil { + return nil, err + } + obj = append(obj, yaml.MapItem{Key: objectKeyI, Value: objectValueI}) + } + return obj, nil + case ']': + return nil, errClosingArrayDelim + case '}': + return nil, errClosingObjectDelim + default: + return nil, fmt.Errorf("unrecognized delimiter") + } + default: + return nil, fmt.Errorf("unrecognized token type %T", tok) + } +} diff --git a/safeyaml/jsontoyaml_test.go b/safeyaml/jsontoyaml_test.go new file mode 100644 index 000000000..2ec19f475 --- /dev/null +++ b/safeyaml/jsontoyaml_test.go @@ -0,0 +1,50 @@ +package safeyaml + +import ( + "testing" + + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v2" +) + +func TestJSONtoYAML(t *testing.T) { + for _, test := range []struct { + Name string + JSON string + YAML string + }{ + { + Name: "object", + JSON: `{"z":"a", "y":"b", "x": 1, "w": 1.2, "v": {"foo": "bar", "baz": "qux"}}`, + YAML: "z: a\n\"y\": b\nx: 1\nw: 1.2\nv:\n foo: bar\n baz: qux\n", + }, + { + Name: "slice", + JSON: `["a", 1, {"foo": "bar"}]`, + YAML: "- a\n- 1\n- foo: bar\n", + }, + { + Name: "empty slice", + JSON: `[]`, + YAML: "[]\n", + }, + { + Name: "empty object", + JSON: `{}`, + YAML: "{}\n", + }, + { + Name: "nil", + JSON: `{"foo": null}`, + YAML: "foo: null\n", + }, + } { + t.Run(test.Name, func(t *testing.T) { + obj, err := JSONtoYAML([]byte(test.JSON)) + require.NoError(t, err) + out, err := yaml.Marshal(obj) + require.NoError(t, err) + require.Equal(t, test.YAML, string(out)) + }) + } +}