Skip to content

Commit 6872eb0

Browse files
committed
Add JSONObjectToYAMLObject
1 parent fd68e98 commit 6872eb0

File tree

2 files changed

+183
-0
lines changed

2 files changed

+183
-0
lines changed

yaml.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,3 +317,64 @@ func convertToJSONableObject(yamlObj interface{}, jsonTarget *reflect.Value) (in
317317
return yamlObj, nil
318318
}
319319
}
320+
321+
// JSONObjectToYAMLObject converts an in-memory JSON object into a YAML in-memory MapSlice,
322+
// without going through a byte representation. A nil or empty map[string]interface{} input is
323+
// converted to an empty map, i.e. yaml.MapSlice(nil).
324+
//
325+
// interface{} slices stay interface{} slices. map[string]interface{} becomes yaml.MapSlice.
326+
//
327+
// int64 and float64 are down casted following the logic of github.com/go-yaml/yaml:
328+
// - float64s are down-casted as far as possible without data-loss to int, int64, uint64.
329+
// - int64s are down-casted to int if possible without data-loss.
330+
//
331+
// Big int/int64/uint64 do not lose precision as in the json-yaml roundtripping case.
332+
//
333+
// string, bool and any other types are unchanged.
334+
func JSONObjectToYAMLObject(j map[string]interface{}) yaml.MapSlice {
335+
if len(j) == 0 {
336+
return nil
337+
}
338+
ret := make(yaml.MapSlice, 0, len(j))
339+
for k, v := range j {
340+
ret = append(ret, yaml.MapItem{Key: k, Value: jsonToYAMLValue(v)})
341+
}
342+
return ret
343+
}
344+
345+
func jsonToYAMLValue(j interface{}) interface{} {
346+
switch j := j.(type) {
347+
case map[string]interface{}:
348+
if j == nil {
349+
return interface{}(nil)
350+
}
351+
return JSONObjectToYAMLObject(j)
352+
case []interface{}:
353+
if j == nil {
354+
return interface{}(nil)
355+
}
356+
ret := make([]interface{}, len(j))
357+
for i := range j {
358+
ret[i] = jsonToYAMLValue(j[i])
359+
}
360+
return ret
361+
case float64:
362+
// replicate the logic in https://github.com/go-yaml/yaml/blob/51d6538a90f86fe93ac480b35f37b2be17fef232/resolve.go#L151
363+
if i64 := int64(j); j == float64(i64) {
364+
if i := int(i64); i64 == int64(i) {
365+
return i
366+
}
367+
return i64
368+
}
369+
if ui64 := uint64(j); j == float64(ui64) {
370+
return ui64
371+
}
372+
return j
373+
case int64:
374+
if i := int(j); j == int64(i) {
375+
return i
376+
}
377+
return j
378+
}
379+
return j
380+
}

yaml_test.go

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
package yaml
22

33
import (
4+
"encoding/json"
45
"fmt"
56
"math"
67
"reflect"
8+
"sort"
79
"strconv"
810
"testing"
11+
12+
"github.com/davecgh/go-spew/spew"
13+
yaml "gopkg.in/yaml.v2"
914
)
1015

1116
type MarshalTest struct {
@@ -421,3 +426,120 @@ foo: baz
421426
t.Error("expected YAMLtoJSONStrict to fail on duplicate field names")
422427
}
423428
}
429+
430+
func TestJSONObjectToYAMLObject(t *testing.T) {
431+
intOrInt64 := func(i64 int64) interface{} {
432+
if i := int(i64); i64 == int64(i) {
433+
return i
434+
}
435+
return i64
436+
}
437+
438+
tests := []struct {
439+
name string
440+
input map[string]interface{}
441+
expected yaml.MapSlice
442+
}{
443+
{name: "nil", expected: yaml.MapSlice(nil)},
444+
{name: "empty", input: map[string]interface{}{}, expected: yaml.MapSlice(nil)},
445+
{
446+
name: "values",
447+
input: map[string]interface{}{
448+
"nil slice": []interface{}(nil),
449+
"nil map": map[string]interface{}(nil),
450+
"empty slice": []interface{}{},
451+
"empty map": map[string]interface{}{},
452+
"bool": true,
453+
"float64": float64(42.1),
454+
"fractionless": float64(42),
455+
"int": int(42),
456+
"int64": int64(42),
457+
"int64 big": float64(math.Pow(2, 62)),
458+
"negative int64 big": -float64(math.Pow(2, 62)),
459+
"map": map[string]interface{}{"foo": "bar"},
460+
"slice": []interface{}{"foo", "bar"},
461+
"string": string("foo"),
462+
"uint64 big": float64(math.Pow(2, 63)),
463+
},
464+
expected: yaml.MapSlice{
465+
{Key: "nil slice"},
466+
{Key: "nil map"},
467+
{Key: "empty slice", Value: []interface{}{}},
468+
{Key: "empty map", Value: yaml.MapSlice(nil)},
469+
{Key: "bool", Value: true},
470+
{Key: "float64", Value: float64(42.1)},
471+
{Key: "fractionless", Value: int(42)},
472+
{Key: "int", Value: int(42)},
473+
{Key: "int64", Value: int(42)},
474+
{Key: "int64 big", Value: intOrInt64(int64(1) << 62)},
475+
{Key: "negative int64 big", Value: intOrInt64(-(1 << 62))},
476+
{Key: "map", Value: yaml.MapSlice{{Key: "foo", Value: "bar"}}},
477+
{Key: "slice", Value: []interface{}{"foo", "bar"}},
478+
{Key: "string", Value: string("foo")},
479+
{Key: "uint64 big", Value: uint64(1) << 63},
480+
},
481+
},
482+
}
483+
for _, tt := range tests {
484+
t.Run(tt.name, func(t *testing.T) {
485+
got := JSONObjectToYAMLObject(tt.input)
486+
sortMapSlicesInPlace(tt.expected)
487+
sortMapSlicesInPlace(got)
488+
if !reflect.DeepEqual(got, tt.expected) {
489+
t.Errorf("jsonToYAML() = %v, want %v", spew.Sdump(got), spew.Sdump(tt.expected))
490+
}
491+
492+
jsonBytes, err := json.Marshal(tt.input)
493+
if err != nil {
494+
t.Fatalf("unexpected json.Marshal error: %v", err)
495+
}
496+
var gotByRoundtrip yaml.MapSlice
497+
if err := yaml.Unmarshal(jsonBytes, &gotByRoundtrip); err != nil {
498+
t.Fatalf("unexpected yaml.Unmarshal error: %v", err)
499+
}
500+
501+
// yaml.Unmarshal loses precision, it's rounding to the 4th last digit.
502+
// Replicate this here in the test, but don't change the type.
503+
for i := range got {
504+
switch got[i].Key {
505+
case "int64 big", "uint64 big", "negative int64 big":
506+
switch v := got[i].Value.(type) {
507+
case int64:
508+
d := int64(500)
509+
if v < 0 {
510+
d = -500
511+
}
512+
got[i].Value = int64((v+d)/1000) * 1000
513+
case uint64:
514+
got[i].Value = uint64((v+500)/1000) * 1000
515+
case int:
516+
d := int(500)
517+
if v < 0 {
518+
d = -500
519+
}
520+
got[i].Value = int((v+d)/1000) * 1000
521+
default:
522+
t.Fatalf("unexpected type for key %s: %v:%T", got[i].Key, v, v)
523+
}
524+
}
525+
}
526+
527+
if !reflect.DeepEqual(got, gotByRoundtrip) {
528+
t.Errorf("yaml.Unmarshal(json.Marshal(tt.input)) = %v, want %v\njson: %s", spew.Sdump(gotByRoundtrip), spew.Sdump(got), string(jsonBytes))
529+
}
530+
})
531+
}
532+
}
533+
534+
func sortMapSlicesInPlace(x interface{}) {
535+
switch x := x.(type) {
536+
case []interface{}:
537+
for i := range x {
538+
sortMapSlicesInPlace(x[i])
539+
}
540+
case yaml.MapSlice:
541+
sort.Slice(x, func(a, b int) bool {
542+
return x[a].Key.(string) < x[b].Key.(string)
543+
})
544+
}
545+
}

0 commit comments

Comments
 (0)