Repository: openconfig/ygot
Pull request #601 (2021-10-26 22:33:03)

Add util function for wildcard path matching
* Enable less-strict path comparison for paths with wildcard keys

DanG100: Refactored this a bunch and simplified to follow the use case better. The goal of this func to check if a received gNMI path is valid based on the query path.

Patch:
diff --git a/util/gnmi.go b/util/gnmi.go
index abde761..546fcfe 100644
--- a/util/gnmi.go
+++ b/util/gnmi.go
@@ -43,6 +43,7 @@ func PathMatchesPrefix(path *gpb.Path, prefix []string) bool {
 }
 
 // PathElemsEqual replaces the proto.Equal() check for PathElems.
+// If a.Key["foo"] == "*" and b.Key["foo"] == "bar" func returns false.
 // This significantly improves comparison speed.
 func PathElemsEqual(a, b *gpb.PathElem) bool {
 	// This check allows avoiding to deal with any null PathElems later on.
@@ -53,12 +54,13 @@ func PathElemsEqual(a, b *gpb.PathElem) bool {
 	if a.Name != b.Name {
 		return false
 	}
+
 	if len(a.Key) != len(b.Key) {
 		return false
 	}
 
 	for k, v := range a.Key {
-		if vo, ok := b.Key[k]; !ok || vo != v {
+		if vo, ok := b.Key[k]; !ok || v != vo {
 			return false
 		}
 	}
@@ -80,6 +82,9 @@ func PathElemSlicesEqual(a, b []*gpb.PathElem) bool {
 
 // PathMatchesPathElemPrefix checks whether prefix is a prefix of path. Both paths
 // must use the gNMI >=0.4.0 PathElem path format.
+// Note: Paths must match exactly, that is if path has a wildcard key,
+// then the same key must also be a wildcard in the prefix.
+// See PathMatchesQuery for comparing paths with wildcards.
 func PathMatchesPathElemPrefix(path, prefix *gpb.Path) bool {
 	if len(path.GetElem()) < len(prefix.GetElem()) || path.Origin != prefix.Origin {
 		return false
@@ -92,6 +97,32 @@ func PathMatchesPathElemPrefix(path, prefix *gpb.Path) bool {
 	return true
 }
 
+// PathMatchesQuery returns whether query is prefix of path.
+// Only the query may contain wildcard name or keys.
+// TODO: Multilevel wildcards ("...") not supported.
+// If either path and query contain nil elements func returns false.
+// Both paths must use the gNMI >=0.4.0 PathElem path format.
+func PathMatchesQuery(path, query *gpb.Path) bool {
+	if len(path.GetElem()) < len(query.GetElem()) || path.Origin != query.Origin {
+		return false
+	}
+	for i, queryElem := range query.Elem {
+		pathElem := path.Elem[i]
+		if queryElem == nil || pathElem == nil {
+			return false
+		}
+		if queryElem.Name != "*" && queryElem.Name != pathElem.Name {
+			return false
+		}
+		for qk, qv := range queryElem.Key {
+			if pv, ok := pathElem.Key[qk]; !ok || (qv != "*" && qv != pv) {
+				return false
+			}
+		}
+	}
+	return true
+}
+
 // TrimGNMIPathPrefix returns path with the prefix trimmed. It returns the
 // original path if the prefix does not fully match.
 func TrimGNMIPathPrefix(path *gpb.Path, prefix []string) *gpb.Path {


Test patch:
diff --git a/util/gnmi_test.go b/util/gnmi_test.go
index 871c9b4..9df882a 100644
--- a/util/gnmi_test.go
+++ b/util/gnmi_test.go
@@ -526,6 +526,200 @@ func TestPathMatchesPathElemPrefix(t *testing.T) {
 	}
 }
 
+func TestPathMatchesQuery(t *testing.T) {
+	tests := []struct {
+		desc    string
+		inPath  *gpb.Path
+		inQuery *gpb.Path
+		want    bool
+	}{{
+		desc: "valid query with no keys",
+		inPath: &gpb.Path{
+			Elem: []*gpb.PathElem{{
+				Name: "one",
+			}, {
+				Name: "two",
+			}},
+		},
+		inQuery: &gpb.Path{
+			Elem: []*gpb.PathElem{{
+				Name: "one",
+			}},
+		},
+		want: true,
+	}, {
+		desc: "valid query with wildcard name",
+		inPath: &gpb.Path{
+			Elem: []*gpb.PathElem{{
+				Name: "one",
+			}, {
+				Name: "two",
+			}},
+		},
+		inQuery: &gpb.Path{
+			Elem: []*gpb.PathElem{{
+				Name: "*",
+			}, {
+				Name: "two",
+			}},
+		},
+		want: true,
+	}, {
+		desc: "valid query with exact key match",
+		inPath: &gpb.Path{
+			Elem: []*gpb.PathElem{{
+				Name: "one",
+				Key:  map[string]string{"two": "three"},
+			}, {
+				Name: "four",
+			}},
+		},
+		inQuery: &gpb.Path{
+			Elem: []*gpb.PathElem{{
+				Name: "one",
+				Key:  map[string]string{"two": "three"},
+			}},
+		},
+		want: true,
+	}, {
+		desc: "valid query with wildcard keys",
+		inPath: &gpb.Path{
+			Elem: []*gpb.PathElem{{
+				Name: "one",
+				Key:  map[string]string{"two": "three"},
+			}, {
+				Name: "four",
+			}},
+		},
+		inQuery: &gpb.Path{
+			Elem: []*gpb.PathElem{{
+				Name: "one",
+				Key:  map[string]string{"two": "*"},
+			}},
+		},
+		want: true,
+	}, {
+		desc: "valid query with no keys and path with keys",
+		inPath: &gpb.Path{
+			Elem: []*gpb.PathElem{{
+				Name: "one",
+				Key:  map[string]string{"two": "three"},
+			}, {
+				Name: "four",
+			}},
+		},
+		inQuery: &gpb.Path{
+			Elem: []*gpb.PathElem{{
+				Name: "one",
+			}},
+		},
+		want: true,
+	}, {
+		desc: "valid query with both missing and wildcard keys",
+		inPath: &gpb.Path{
+			Elem: []*gpb.PathElem{{
+				Name: "one",
+				Key: map[string]string{
+					"two":  "three",
+					"four": "five",
+				},
+			}, {
+				Name: "four",
+			}},
+		},
+		inQuery: &gpb.Path{
+			Elem: []*gpb.PathElem{{
+				Name: "one",
+				Key:  map[string]string{"four": "*"},
+			}},
+		},
+		want: true,
+	}, {
+		desc: "invalid nil elements",
+		inPath: &gpb.Path{
+			Elem: []*gpb.PathElem{
+				nil,
+				{
+					Name: "twelve",
+				}},
+		},
+		inQuery: &gpb.Path{
+			Elem: []*gpb.PathElem{{
+				Name: "three",
+			}},
+		},
+	}, {
+		desc: "invalid names not equal",
+		inPath: &gpb.Path{
+			Elem: []*gpb.PathElem{{
+				Name: "fourteen",
+			}, {
+				Name: "twelve",
+			}},
+		},
+		inQuery: &gpb.Path{
+			Elem: []*gpb.PathElem{{
+				Name: "three",
+			}},
+		},
+	}, {
+		desc: "invalid origin",
+		inPath: &gpb.Path{
+			Origin: "openconfig",
+			Elem: []*gpb.PathElem{{
+				Name: "one",
+			}, {
+				Name: "two",
+			}},
+		},
+		inQuery: &gpb.Path{
+			Origin: "google",
+			Elem: []*gpb.PathElem{{
+				Name: "one",
+			}},
+		},
+	}, {
+		desc: "invalid keys",
+		inPath: &gpb.Path{
+			Elem: []*gpb.PathElem{{
+				Name: "three",
+				Key:  map[string]string{"four": "five"},
+			}, {
+				Name: "six",
+			}},
+		},
+		inQuery: &gpb.Path{
+			Elem: []*gpb.PathElem{{
+				Name: "three",
+				Key:  map[string]string{"seven": "eight"},
+			}},
+		},
+	}, {
+		desc: "invalid missing wildcard keys",
+		inPath: &gpb.Path{
+			Elem: []*gpb.PathElem{{
+				Name: "three",
+				Key:  map[string]string{"four": "five"},
+			}, {
+				Name: "six",
+			}},
+		},
+		inQuery: &gpb.Path{
+			Elem: []*gpb.PathElem{{
+				Name: "three",
+				Key:  map[string]string{"seven": "*"},
+			}},
+		},
+	}}
+	for _, tt := range tests {
+		t.Run(tt.desc, func(t *testing.T) {
+			if got := PathMatchesQuery(tt.inPath, tt.inQuery); got != tt.want {
+				t.Fatalf("did not get expected result, got: %v, want: %v", got, tt.want)
+			}
+		})
+	}
+}
+
 func TestTrimGNMIPathElemPrefix(t *testing.T) {
 	tests := []struct {
 		desc     string