Skip to content

Commit b7de07a

Browse files
authored
Change internal JSON Generation for OrderedMaps to be backwards-compatible (#894)
* Fix BuildEmptyTree for ordered maps * Fix internal JSON
1 parent db25a8c commit b7de07a

File tree

3 files changed

+120
-95
lines changed

3 files changed

+120
-95
lines changed

ygot/render.go

Lines changed: 91 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,13 @@ import (
1919
"errors"
2020
"fmt"
2121
"reflect"
22-
"sort"
2322
"strings"
2423

2524
"github.com/openconfig/gnmi/errlist"
2625
"github.com/openconfig/gnmi/value"
2726
"github.com/openconfig/ygot/internal/yreflect"
2827
"github.com/openconfig/ygot/util"
28+
"golang.org/x/exp/slices"
2929
"google.golang.org/protobuf/encoding/prototext"
3030
"google.golang.org/protobuf/proto"
3131

@@ -1442,74 +1442,68 @@ func keyValue(v reflect.Value, prependModuleNameIref bool) (any, error) {
14421442
return name, nil
14431443
}
14441444

1445-
// mapJSON takes an input reflect.Value containing a map, and
1446-
// constructs the representation for JSON marshalling that corresponds to it.
1447-
// The module within which the map is defined is specified by the parentMod
1448-
// argument.
1449-
func mapJSON(field reflect.Value, parentMod string, args jsonOutputConfig) (any, error) {
1450-
var errs errlist.List
1451-
mapKeyMap := map[string]reflect.Value{}
1452-
// Order of elements determines the order in which keys will be processed.
1453-
var mapKeys []string
1445+
// mapKeyToJSONString converts a map key to a string to be used in JSON output.
1446+
func mapKeyToJSONString(k reflect.Value, args jsonOutputConfig) (string, error) {
14541447
switch args.jType {
14551448
case RFC7951:
1456-
// YANG lists are marshalled into a JSON object array for IETF
1457-
// JSON. We handle the keys in alphabetical order to ensure that
1458-
// deterministic ordering is achieved in the output JSON.
1459-
for _, k := range field.MapKeys() {
1460-
keyval, err := keyValue(k, false)
1461-
if err != nil {
1462-
errs.Add(fmt.Errorf("invalid enumerated key: %v", err))
1463-
continue
1464-
}
1465-
kn := fmt.Sprintf("%v", keyval)
1466-
mapKeys = append(mapKeys, kn)
1467-
mapKeyMap[kn] = k
1449+
k, err := keyValue(k, false)
1450+
if err != nil {
1451+
return "", fmt.Errorf("invalid enumerated key: %v", err)
14681452
}
1453+
return fmt.Sprintf("%v", k), nil
14691454
case Internal:
14701455
// In non-IETF JSON, then we output a list as a JSON object. The keys
14711456
// are stored as strings.
1472-
for _, k := range field.MapKeys() {
1473-
var kn string
1474-
switch k.Kind() {
1475-
case reflect.Struct:
1476-
// Handle the case of a multikey list.
1477-
var kp []string
1478-
for j := 0; j < k.NumField(); j++ {
1479-
keyval, err := keyValue(k.Field(j), false)
1480-
if err != nil {
1481-
errs.Add(fmt.Errorf("invalid enumerated key: %v", err))
1482-
continue
1483-
}
1484-
kp = append(kp, fmt.Sprintf("%v", keyval))
1485-
}
1486-
kn = strings.Join(kp, " ")
1487-
case reflect.Int64:
1488-
keyval, err := keyValue(k, false)
1457+
switch k.Kind() {
1458+
case reflect.Struct:
1459+
var errs errlist.List
1460+
// Handle the case of a multikey list.
1461+
var kp []string
1462+
for j := 0; j < k.NumField(); j++ {
1463+
keyval, err := keyValue(k.Field(j), false)
14891464
if err != nil {
14901465
errs.Add(fmt.Errorf("invalid enumerated key: %v", err))
14911466
continue
14921467
}
1493-
kn = fmt.Sprintf("%v", keyval)
1494-
default:
1495-
kn = fmt.Sprintf("%v", k.Interface())
1468+
kp = append(kp, fmt.Sprintf("%v", keyval))
1469+
}
1470+
if errs.Err() != nil {
1471+
return "", errs.Err()
1472+
}
1473+
return strings.Join(kp, " "), nil
1474+
case reflect.Int64:
1475+
keyval, err := keyValue(k, false)
1476+
if err != nil {
1477+
return "", fmt.Errorf("invalid enumerated key: %v", err)
14961478
}
1497-
mapKeys = append(mapKeys, kn)
1498-
mapKeyMap[kn] = k
1479+
return fmt.Sprintf("%v", keyval), nil
1480+
default:
1481+
return fmt.Sprintf("%v", k.Interface()), nil
14991482
}
15001483
default:
1501-
return nil, fmt.Errorf("unknown JSON type: %v", args.jType)
1484+
return "", fmt.Errorf("unknown JSON type: %v", args.jType)
15021485
}
1503-
sort.Strings(mapKeys)
1486+
}
1487+
1488+
// mapValuePair represents a map value pair with the key as a string.
1489+
type mapValuePair struct {
1490+
k string
1491+
v reflect.Value
1492+
}
15041493

1505-
if len(mapKeys) == 0 {
1494+
// mapValuePairsToJSON converts the given ordered pair of map values to JSON
1495+
// according to the JSON format option.
1496+
func mapValuePairsToJSON(pairs []mapValuePair, parentMod string, args jsonOutputConfig) (any, error) {
1497+
if len(pairs) == 0 {
15061498
// empty list should be encoded as empty list
15071499
if args.jType == RFC7951 {
15081500
return []any{}, nil
15091501
}
15101502
return nil, nil
15111503
}
15121504

1505+
var errs errlist.List
1506+
15131507
// Build the output that we expect. Since there is a difference between the IETF
15141508
// and non-IETF forms, we simply choose vals to be any, and then type assert
15151509
// it later on. Since t cannot mutuate through this function we can guarantee that
@@ -1524,11 +1518,10 @@ func mapJSON(field reflect.Value, parentMod string, args jsonOutputConfig) (any,
15241518
default:
15251519
return nil, fmt.Errorf("invalid JSON format specified: %v", args.jType)
15261520
}
1527-
for _, kn := range mapKeys {
1528-
k := mapKeyMap[kn]
1529-
goStruct, ok := field.MapIndex(k).Interface().(GoStruct)
1521+
for _, pair := range pairs {
1522+
goStruct, ok := pair.v.Interface().(GoStruct)
15301523
if !ok {
1531-
errs.Add(fmt.Errorf("cannot map struct %v, invalid GoStruct", field))
1524+
errs.Add(fmt.Errorf("cannot map struct %v, invalid GoStruct", pair.v.Interface()))
15321525
continue
15331526
}
15341527

@@ -1542,19 +1535,46 @@ func mapJSON(field reflect.Value, parentMod string, args jsonOutputConfig) (any,
15421535
case RFC7951:
15431536
vals = append(vals.([]any), val)
15441537
case Internal:
1545-
vals.(map[string]any)[kn] = val
1538+
vals.(map[string]any)[pair.k] = val
15461539
default:
15471540
errs.Add(fmt.Errorf("invalid JSON type: %v", args.jType))
15481541
continue
15491542
}
15501543
}
1551-
15521544
if errs.Err() != nil {
15531545
return nil, errs.Err()
15541546
}
15551547
return vals, nil
15561548
}
15571549

1550+
// mapJSON takes an input reflect.Value containing a map, and
1551+
// constructs the representation for JSON marshalling that corresponds to it.
1552+
// The module within which the map is defined is specified by the parentMod
1553+
// argument.
1554+
func mapJSON(field reflect.Value, parentMod string, args jsonOutputConfig) (any, error) {
1555+
var errs errlist.List
1556+
// Order of elements determines the order in which keys will be processed.
1557+
var pairs []mapValuePair
1558+
1559+
iter := field.MapRange()
1560+
for iter.Next() {
1561+
kn, err := mapKeyToJSONString(iter.Key(), args)
1562+
if err != nil {
1563+
errs.Add(err)
1564+
continue
1565+
}
1566+
pairs = append(pairs, mapValuePair{k: kn, v: iter.Value()})
1567+
}
1568+
slices.SortFunc(pairs, func(a, b mapValuePair) bool { return a.k < b.k })
1569+
1570+
js, err := mapValuePairsToJSON(pairs, parentMod, args)
1571+
errs.Add(err)
1572+
if errs.Err() != nil {
1573+
return nil, errs.Err()
1574+
}
1575+
return js, nil
1576+
}
1577+
15581578
// jsonValue takes a reflect.Value which represents a struct field and
15591579
// constructs the representation that can be used to marshal the field to JSON.
15601580
// The module within which the value is defined is specified by the parentMod string,
@@ -1581,21 +1601,26 @@ func jsonValue(field reflect.Value, parentMod string, args jsonOutputConfig) (an
15811601
errs.Add(err)
15821602
}
15831603
case reflect.Ptr:
1584-
if _, ok := field.Interface().(GoOrderedMap); ok {
1585-
// This is an ordered-map for YANG "ordered-by user" lists.
1586-
valuesMethod, err := yreflect.MethodByName(field, "Values")
1587-
if err != nil {
1588-
return nil, err
1589-
}
1590-
ret := valuesMethod.Call(nil)
1591-
if got, wantReturnN := len(ret), 1; got != wantReturnN {
1592-
return nil, fmt.Errorf("method Values() doesn't have expected number of return values, got %v, want %v", got, wantReturnN)
1604+
if om, ok := field.Interface().(GoOrderedMap); ok {
1605+
var pairs []mapValuePair
1606+
if err := yreflect.RangeOrderedMap(om, func(k reflect.Value, v reflect.Value) bool {
1607+
kn, err := mapKeyToJSONString(k, args)
1608+
if err != nil {
1609+
errs.Add(err)
1610+
return true
1611+
}
1612+
pairs = append(pairs, mapValuePair{k: kn, v: v})
1613+
return true
1614+
}); err != nil {
1615+
errs.Add(err)
15931616
}
1594-
values := ret[0]
1595-
if gotKind := values.Type().Kind(); gotKind != reflect.Slice {
1596-
return nil, fmt.Errorf("method Values() did not return a slice value, got %v", gotKind)
1617+
1618+
js, err := mapValuePairsToJSON(pairs, parentMod, args)
1619+
errs.Add(err)
1620+
if errs.Err() != nil {
1621+
return nil, errs.Err()
15971622
}
1598-
return jsonValue(values, parentMod, args)
1623+
return js, nil
15991624
}
16001625

16011626
switch field.Elem().Kind() {

ygot/render_exported_test.go

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -363,8 +363,8 @@ func TestConstructJSONOrderedMap(t *testing.T) {
363363
},
364364
wantInternal: map[string]any{
365365
"ordered-multikeyed-lists": map[string]any{
366-
"ordered-multikeyed-list": []any{
367-
map[string]any{
366+
"ordered-multikeyed-list": map[string]any{
367+
"foo 42": map[string]any{
368368
"key1": "foo",
369369
"key2": uint64(42),
370370
"config": map[string]any{
@@ -376,7 +376,7 @@ func TestConstructJSONOrderedMap(t *testing.T) {
376376
"ro-value": "foo-state-val",
377377
},
378378
},
379-
map[string]any{
379+
"bar 42": map[string]any{
380380
"key1": "bar",
381381
"key2": uint64(42),
382382
"config": map[string]any{
@@ -388,7 +388,7 @@ func TestConstructJSONOrderedMap(t *testing.T) {
388388
"ro-value": "bar-state-val",
389389
},
390390
},
391-
map[string]any{
391+
"baz 84": map[string]any{
392392
"key1": "baz",
393393
"key2": uint64(84),
394394
"config": map[string]any{
@@ -454,8 +454,8 @@ func TestConstructJSONOrderedMap(t *testing.T) {
454454
},
455455
wantInternal: map[string]any{
456456
"ordered-multikeyed-lists": map[string]any{
457-
"ordered-multikeyed-list": []any{
458-
map[string]any{
457+
"ordered-multikeyed-list": map[string]any{
458+
"foo 42": map[string]any{
459459
"key1": "foo",
460460
"key2": uint64(42),
461461
"config": map[string]any{
@@ -465,15 +465,15 @@ func TestConstructJSONOrderedMap(t *testing.T) {
465465
"ro-value": "foo-state-val",
466466
},
467467
},
468-
map[string]any{
468+
"bar 42": map[string]any{
469469
"key1": "bar",
470470
"key2": uint64(42),
471471
"state": map[string]any{
472472
"value": "bar-val",
473473
"ro-value": "bar-state-val",
474474
},
475475
},
476-
map[string]any{
476+
"baz 84": map[string]any{
477477
"key1": "baz",
478478
"key2": uint64(84),
479479
"config": map[string]any{
@@ -560,22 +560,22 @@ func TestEncodeTypedValueOrderedMap(t *testing.T) {
560560
}{{
561561
name: "ordered list type",
562562
inVal: ctestschema.GetOrderedMap(t),
563-
want: &gnmipb.TypedValue{Value: &gnmipb.TypedValue_JsonVal{JsonVal: []byte(`[
564-
{
565-
"config": {
566-
"key": "foo",
567-
"value": "foo-val"
568-
},
569-
"key": "foo"
570-
},
571-
{
563+
want: &gnmipb.TypedValue{Value: &gnmipb.TypedValue_JsonVal{JsonVal: []byte(`{
564+
"bar": {
572565
"config": {
573566
"key": "bar",
574567
"value": "bar-val"
575568
},
576569
"key": "bar"
570+
},
571+
"foo": {
572+
"config": {
573+
"key": "foo",
574+
"value": "foo-val"
575+
},
576+
"key": "foo"
577577
}
578-
]`)}},
578+
}`)}},
579579
}, {
580580
name: "ordered list type - ietf json",
581581
inVal: ctestschema.GetOrderedMap(t),
Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,27 @@
11
{
22
"ordered-lists": {
3-
"ordered-list": [
4-
{
5-
"config": {
6-
"key": "foo",
7-
"value": "foo-val"
8-
},
9-
"key": "foo"
10-
},
11-
{
3+
"ordered-list": {
4+
"bar": {
125
"config": {
136
"key": "bar",
147
"value": "bar-val"
158
},
169
"key": "bar"
1710
},
18-
{
11+
"baz": {
1912
"config": {
2013
"key": "baz",
2114
"value": "baz-val"
2215
},
2316
"key": "baz"
17+
},
18+
"foo": {
19+
"config": {
20+
"key": "foo",
21+
"value": "foo-val"
22+
},
23+
"key": "foo"
2424
}
25-
]
25+
}
2626
}
2727
}

0 commit comments

Comments
 (0)