Skip to content

Commit c238422

Browse files
Fix leaf-list validation when it is a relative path leafref (#626)
* workaround leafref validation * as commented by @wenovus in #623 The fix is essentially correct: we handle the case of lists (a single level in YANG) being represented as two levels in Go (map -> element), but don't handle the case of leaf-lists also being represented as two levels in Go (slice -> element). The "two levels" is how ForEachField processes elements, which creates this corner case for DataNodeAtPath. It turns out we already had a test case for this but it had an extra "../" s o it was testing the wrong behaviour. * Do not trim compressed path elements for maps or slices. Currently, we trim the path for a `map[]{}` or slice type in Go for lists in order to prevent the list's name from appearing twice in the data tree while traversing a list element. This is because `forEachFieldInternal` currently creates new `NodeInfo` elements, copying the list name, when traversing list elements. Based on the tests that fail, this behaviour apparently tries to solve the problem of processing "../" when calling `ytypes.dataNodesAtPath`, but the logic in there incorrectly identifies compression to be the issue: compression is indeed not the issue, but rather how slices and maps are processed in two levels -- slices and maps exist equally in compressed and uncompressed generated code. This PR removes the dependency of existing logic on schema compression, but rather on whether the current element is a map or a slice, corresponding to a YANG list or leaf-list, and in such cases, it skips that extra level created by `forEachFieldInternal` instead. This results in slightly simpler, and much more understandable logic with the comment updates. * try using go install * misspelling * Add a sentence on why compression is not involved Co-authored-by: Hans Thienpondt <[email protected]>
1 parent 444fa8c commit c238422

File tree

4 files changed

+169
-27
lines changed

4 files changed

+169
-27
lines changed

util/reflect.go

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -681,12 +681,6 @@ func forEachFieldInternal(ni *NodeInfo, in, out interface{}, iterFunction FieldI
681681
continue
682682
}
683683
nn.PathFromParent = p
684-
// In the case of a map/slice, the path is of the form
685-
// "container/element" in the compressed schema, so trim off
686-
// any extra path elements in this case.
687-
if IsTypeSlice(sf.Type) || IsTypeMap(sf.Type) {
688-
nn.PathFromParent = p[0:1]
689-
}
690684
switch in.(type) {
691685
case *PathQueryNodeMemo: // Memoization of path queries requested.
692686
errs = AppendErrs(errs, forEachFieldInternal(nn, newPathQueryMemo(), out, iterFunction))

util/reflect_test.go

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1029,6 +1029,14 @@ type BasicSliceStruct struct {
10291029
StringSlice []string `path:"strlist"`
10301030
}
10311031

1032+
type BasicSliceCompressed struct {
1033+
StringSlice []string `path:"config/strlist"`
1034+
}
1035+
1036+
type BasicStructCompressed struct {
1037+
BasicStructPtrMapField map[string]*BasicStruct `path:"basic-structs/basic-struct"`
1038+
}
1039+
10321040
type StructOfStructs struct {
10331041
BasicStructField BasicStruct `path:"basic-struct"`
10341042
BasicStructPtrField *BasicStruct `path:"basic-struct"`
@@ -1152,6 +1160,67 @@ func TestForEachField(t *testing.T) {
11521160
},
11531161
}
11541162

1163+
compressedStructSchema := &yang.Entry{
1164+
Name: "compressedStruct",
1165+
Kind: yang.DirectoryEntry,
1166+
Dir: map[string]*yang.Entry{
1167+
"basic-structs": {
1168+
Name: "basic-structs",
1169+
Kind: yang.DirectoryEntry,
1170+
ListAttr: yang.NewDefaultListAttr(),
1171+
Dir: map[string]*yang.Entry{
1172+
"basic-struct": {
1173+
Name: "basic-struct",
1174+
Kind: yang.DirectoryEntry,
1175+
Dir: map[string]*yang.Entry{
1176+
"int32": {
1177+
Kind: yang.LeafEntry,
1178+
Name: "int32",
1179+
Type: &yang.YangType{Kind: yang.Yint32},
1180+
},
1181+
"string": {
1182+
Kind: yang.LeafEntry,
1183+
Name: "string",
1184+
Type: &yang.YangType{Kind: yang.Ystring},
1185+
},
1186+
"int32ptr": {
1187+
Kind: yang.LeafEntry,
1188+
Name: "int32ptr",
1189+
Type: &yang.YangType{Kind: yang.Yint32},
1190+
},
1191+
"stringptr": {
1192+
Kind: yang.LeafEntry,
1193+
Name: "stringptr",
1194+
Type: &yang.YangType{Kind: yang.Ystring},
1195+
},
1196+
},
1197+
},
1198+
},
1199+
},
1200+
},
1201+
}
1202+
1203+
compressedLeafListStructSchema := &yang.Entry{
1204+
Name: "leafListStruct",
1205+
Kind: yang.DirectoryEntry,
1206+
Dir: map[string]*yang.Entry{
1207+
"config": {
1208+
Name: "config",
1209+
Kind: yang.DirectoryEntry,
1210+
Dir: map[string]*yang.Entry{
1211+
"strlist": {
1212+
Name: "strlist",
1213+
Kind: yang.LeafEntry,
1214+
Type: &yang.YangType{
1215+
Kind: yang.Ystring,
1216+
},
1217+
ListAttr: yang.NewDefaultListAttr(),
1218+
},
1219+
},
1220+
},
1221+
},
1222+
}
1223+
11551224
tests := []struct {
11561225
desc string
11571226
schema *yang.Entry
@@ -1203,6 +1272,24 @@ func TestForEachField(t *testing.T) {
12031272
iterFunc: printFieldsIterFunc,
12041273
wantOut: `Int32Field : 42, StringField : "forty two", Int32PtrField : 4242, StringPtrField : "forty two ptr", Int32Field : 43, StringField : "forty three", Int32PtrField : 4343, StringPtrField : "forty three ptr", `,
12051274
},
1275+
{
1276+
desc: "struct of map of structs",
1277+
schema: compressedStructSchema,
1278+
parentStruct: &BasicStructCompressed{BasicStructPtrMapField: map[string]*BasicStruct{"basicStruct2": &basicStruct2}},
1279+
in: nil,
1280+
iterFunc: func(ni *NodeInfo, in, out interface{}) (errs Errors) {
1281+
// Only print basic scalar values, skip everything else.
1282+
if !IsValueScalar(ni.FieldValue) || IsValueNil(ni.FieldKey) {
1283+
return
1284+
}
1285+
outs := out.(*string)
1286+
// Print out ni.Parent.Parent.PathFromParent since that's the list's parent path for BasicStruct.
1287+
// This is because ForEachField traverses at the slice/map's level and then at the element level.
1288+
*outs += fmt.Sprintf("%v : %v : %v, ", ni.Parent.Parent.PathFromParent, ni.StructField.Name, pretty.Sprint(ni.FieldValue.Interface()))
1289+
return
1290+
},
1291+
wantOut: `[basic-structs basic-struct] : Int32Field : 43, [basic-structs basic-struct] : StringField : "forty three", [basic-structs basic-struct] : Int32PtrField : 4343, [basic-structs basic-struct] : StringPtrField : "forty three ptr", `,
1292+
},
12061293
{
12071294
desc: "map keys",
12081295
schema: forEachContainerSchema,
@@ -1221,6 +1308,24 @@ func TestForEachField(t *testing.T) {
12211308
StringPtrField: "forty three ptr"} (string)
12221309
, `,
12231310
},
1311+
{
1312+
desc: "struct with string leaf-list",
1313+
schema: compressedLeafListStructSchema,
1314+
parentStruct: &BasicSliceCompressed{StringSlice: []string{"one", "two"}},
1315+
in: nil,
1316+
iterFunc: func(ni *NodeInfo, in, out interface{}) (errs Errors) {
1317+
// Only print basic scalar values, skip everything else.
1318+
if !IsValueScalar(ni.FieldValue) || IsValueNil(ni.FieldKey) {
1319+
return
1320+
}
1321+
outs := out.(*string)
1322+
// Print out ni.Parent.PathFromParent since that's the slice's parent path.
1323+
// This is because ForEachField traverses at the slice/map's level and then at the element level.
1324+
*outs += fmt.Sprintf("%v : %v : %v, ", ni.Parent.PathFromParent, ni.StructField.Name, pretty.Sprint(ni.FieldValue.Interface()))
1325+
return
1326+
},
1327+
wantOut: `[config strlist] : StringSlice : "one", [config strlist] : StringSlice : "two", `,
1328+
},
12241329
{
12251330
desc: "annotated struct",
12261331
schema: annotatedStructSchema,

ytypes/leafref.go

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -189,23 +189,16 @@ func dataNodesAtPath(ni *util.NodeInfo, path *gpb.Path, pathQueryNode *util.Path
189189
if root.Parent == nil {
190190
return nil, fmt.Errorf("no parent for leafref path at %v, with remaining path %s", ni.Schema.Path(), path)
191191
}
192-
if !util.IsCompressedSchema(root.Schema) && root.Parent.Schema.IsList() && util.IsValueMap(root.Parent.FieldValue) {
193-
// If we are in an uncompressed schema, then we have one more level of the data tree than
194-
// the YANG expects, since our data tree layout is:
195-
// struct (parent container)
196-
// --> map (the list)
197-
// --> struct (the list member)
192+
if (root.Parent.Schema.IsList() && util.IsValueMap(root.Parent.FieldValue)) || (root.Parent.Schema.IsLeafList() && util.IsValueSlice(root.Parent.FieldValue)) {
193+
// YANG lists and YANG leaf-lists are represented as Go maps and slices respectively.
194+
// Despite these being a single level in the YANG hierarchy, util.ForEachField actually
195+
// traverses these elements in two levels: first at the map/slice level, and then at the
196+
// element level. Since it does this by creating a "fake", or extra NodeInfo for each
197+
// element, we need to skip this level of NodeInfo and instead directly use the NodeInfo
198+
// of the parent (i.e. the map or slice) to avoid processing this extra NodeInfo.
198199
//
199-
// In YANG, .. from the list member struct gets us to the parent container, but for us
200-
// we have only reached the map. This means that we end up over-consuming the ".."s. To
201-
// avoid this issue, if we are in an uncompressed schema, and in a list, and we find
202-
// that we're looking at the map, we consume another level of the data tree. This gets
203-
// us to the parent container with ".." as would be expected.
204-
//
205-
// This is NOT required for the compressed schema, because in this case, we have removed
206-
// a level of the data tree. So the parent container in the above example will have been
207-
// removed. This is enforced by ygen. In this case, we do want to consume the extra level
208-
// of ..s in the list case, such that we do not end up under-consuming them.
200+
// Note here that since lists and leaf-lists are represented the same way in compressed
201+
// vs. uncompressed code, this logic is the same regardless of compression.
209202
root = root.Parent
210203
pathQueryRoot = pathQueryRoot.Parent
211204
continue

ytypes/leafref_test.go

Lines changed: 55 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ func TestValidateLeafRefData(t *testing.T) {
152152
Kind: yang.LeafEntry,
153153
Type: &yang.YangType{
154154
Kind: yang.Yleafref,
155-
Path: "../../../leaf-list",
155+
Path: "../../leaf-list",
156156
},
157157
ListAttr: yang.NewDefaultListAttr(),
158158
},
@@ -336,7 +336,7 @@ func TestValidateLeafRefData(t *testing.T) {
336336
LeafList: []*int32{Int32(40), Int32(41), Int32(42)},
337337
Container2: &Container2{LeafListRefToLeafList: []*int32{Int32(41), Int32(42), Int32(43)}},
338338
},
339-
wantErr: `field name LeafListRefToLeafList value 43 (int32 ptr) schema path /leaf-list-ref-to-leaf-list has leafref path ../../../leaf-list not equal to any target nodes`,
339+
wantErr: `field name LeafListRefToLeafList value 43 (int32 ptr) schema path /leaf-list-ref-to-leaf-list has leafref path ../../leaf-list not equal to any target nodes`,
340340
},
341341
{
342342
desc: "keyed list match",
@@ -567,11 +567,21 @@ func TestValidateLeafRefDataCompressedSchemaListOnly(t *testing.T) {
567567
// path "../conf";
568568
// }
569569
// }
570+
// leaf-list conf-ref-list {
571+
// type leafref {
572+
// path "../conf";
573+
// }
574+
// }
570575
// leaf conf2-ref {
571576
// type leafref {
572577
// path "../../../../conf2";
573578
// }
574579
// }
580+
// leaf-list conf2-ref-list {
581+
// type leafref {
582+
// path "../../../../conf2";
583+
// }
584+
// }
575585
// }
576586
// }
577587
// }
@@ -620,6 +630,15 @@ func TestValidateLeafRefDataCompressedSchemaListOnly(t *testing.T) {
620630
Path: "../conf",
621631
},
622632
},
633+
"conf-ref-list": {
634+
Name: "conf-ref-list",
635+
Kind: yang.LeafEntry,
636+
Type: &yang.YangType{
637+
Kind: yang.Yleafref,
638+
Path: "../conf",
639+
},
640+
ListAttr: yang.NewDefaultListAttr(),
641+
},
623642
"conf2-ref": {
624643
Name: "conf2-ref",
625644
Kind: yang.LeafEntry,
@@ -628,6 +647,15 @@ func TestValidateLeafRefDataCompressedSchemaListOnly(t *testing.T) {
628647
Path: "../../../../conf2",
629648
},
630649
},
650+
"conf2-ref-list": {
651+
Name: "conf2-ref-list",
652+
Kind: yang.LeafEntry,
653+
Type: &yang.YangType{
654+
Kind: yang.Yleafref,
655+
Path: "../../../../conf2",
656+
},
657+
ListAttr: yang.NewDefaultListAttr(),
658+
},
631659
},
632660
},
633661
},
@@ -650,9 +678,11 @@ func TestValidateLeafRefDataCompressedSchemaListOnly(t *testing.T) {
650678
rootSchema := containerWithListSchema.Dir["root"]
651679

652680
type RootExample struct {
653-
Conf *uint32 `path:"config/conf|conf"`
654-
ConfRef *uint32 `path:"config/conf-ref"`
655-
Conf2Ref *string `path:"config/conf2-ref"`
681+
Conf *uint32 `path:"config/conf|conf"`
682+
ConfRef *uint32 `path:"config/conf-ref"`
683+
ConfRefList []*uint32 `path:"config/conf-ref-list"`
684+
Conf2Ref *string `path:"config/conf2-ref"`
685+
Conf2RefList []*string `path:"config/conf2-ref-list"`
656686
}
657687

658688
type Root struct {
@@ -712,12 +742,32 @@ func TestValidateLeafRefDataCompressedSchemaListOnly(t *testing.T) {
712742
Example: map[uint32]*RootExample{},
713743
},
714744
},
745+
{
746+
desc: "ref to leaf outside of list (conf2-list)",
747+
in: &Root{
748+
Conf2: String("hitchhiker"),
749+
Example: map[uint32]*RootExample{42: {Conf: Uint32(42), Conf2RefList: []*string{String("hitchhiker")}}},
750+
},
751+
},
752+
{
753+
desc: "empty ref to leaf outside of list (conf2-list)",
754+
in: &Root{
755+
Conf2: String("hitchhiker"),
756+
Example: map[uint32]*RootExample{42: {Conf: Uint32(42), Conf2RefList: []*string{}}},
757+
},
758+
},
715759
{
716760
desc: "conf-ref",
717761
in: &Root{
718762
Example: map[uint32]*RootExample{42: {Conf: Uint32(42), ConfRef: Uint32(42)}},
719763
},
720764
},
765+
{
766+
desc: "conf-ref-list",
767+
in: &Root{
768+
Example: map[uint32]*RootExample{42: {Conf: Uint32(42), ConfRefList: []*uint32{Uint32(42)}}},
769+
},
770+
},
721771
{
722772
desc: "conf-ref unequal to conf",
723773
in: &Root{

0 commit comments

Comments
 (0)