From 55a8b06b10805339d376003d6428ce0acf09afcd Mon Sep 17 00:00:00 2001 From: "Samani G. Gikandi" Date: Wed, 3 Sep 2025 07:51:58 -0700 Subject: [PATCH] xpath: adds support for prefixes in list keys This diff adds support for prefixes in list keys as well as some unit tests. **Context** Where an identifier is something like `interfaces`, an identifier with a prefix might be `openconfig-interfaces:interface`. The ABNF in the YANG RFCs refers to these as `node-identifier` rules. The current xpath implementation does not allow prefixed identifiers in list keys, however per [RFC 6020](https://datatracker.ietf.org/doc/html/rfc6020#section-12) and [RFC 7950](https://datatracker.ietf.org/doc/html/rfc7950#section-14) these are valid. We can look to openconfig for a [real world example](https://github.com/openconfig/public/blob/edf48ec7092fc2be9775937512dac8a1115483fb/release/models/interfaces/openconfig-interfaces.yang#L237-L242). For completeness I've lifted some relevant grammar snippets from the YANG RFCs: ``` identifier = (ALPHA / "_") *(ALPHA / DIGIT / "_" / "-" / ".") prefix = identifier node-identifier = [prefix ":"] identifier absolute-path = 1*("/" (node-identifier *path-predicate)) relative-path = 1*(".." "/") descendant-path descendant-path = node-identifier [*path-predicate absolute-path] key-arg = node-identifier *(sep node-identifier) key-arg-str = < a string that matches the rule key-arg > key-stmt = key-keyword sep key-arg-str stmtend ``` --- utils/xpath/xpath.go | 7 ++++--- utils/xpath/xpath_test.go | 15 +++++++++++++++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/utils/xpath/xpath.go b/utils/xpath/xpath.go index f18f02fe..c7618b4e 100644 --- a/utils/xpath/xpath.go +++ b/utils/xpath/xpath.go @@ -23,16 +23,17 @@ import ( ) var ( - idPattern = `[a-zA-Z_][a-zA-Z\d\_\-\.]*` + idPattern = `[a-zA-Z_][a-zA-Z\d\_\-\.]*` + nodeIdPattern = `(?:` + idPattern + `:)?` + idPattern // YANG identifiers must follow RFC 6020: // https://tools.ietf.org/html/rfc6020#section-6.2. - idRe = regexp.MustCompile(`^(?:`+idPattern+`:)?` + idPattern + `$`) + idRe = regexp.MustCompile(`^` + nodeIdPattern + `$`) // The sting representation of List key value pairs must follow the // following pattern: [key=value], where key is the List key leaf name, // and value is the string representation of key leaf value. kvRe = regexp.MustCompile(`^\[` + // Key leaf name must be a valid YANG identifier. - idPattern + `=` + + nodeIdPattern + `=` + // Key leaf value must be a non-empty string, which may contain // newlines. Use (?s) to turn on s flag to match newlines. `((?s).+)` + diff --git a/utils/xpath/xpath_test.go b/utils/xpath/xpath_test.go index ae844278..a4bbdd47 100644 --- a/utils/xpath/xpath_test.go +++ b/utils/xpath/xpath_test.go @@ -277,6 +277,11 @@ func TestParseElement(t *testing.T) { elem: "a[k1=v1][k2=v2]", expectOK: true, want: []interface{}{"a", map[string]string{"k1": "v1", "k2": "v2"}}, + }, { + desc: "test list with xpath expression in key", + elem: "interface[name=current()]", + expectOK: true, + want: []interface{}{"interface", map[string]string{"name": "current()"}}, }} for _, test := range tests { @@ -360,6 +365,16 @@ func TestParseStringPath(t *testing.T) { }, { desc: "test path containing a multi-key List, second key-value pair without [ and ]", path: "/a/b[k1=10]k2=abc/c", + }, { + desc: "test path containing prefix", + path: "/openconfig-interfaces:interface", + expectOK: true, + want: []interface{}{"openconfig-interfaces:interface"}, + }, { + desc: "test path containing prefix in path and List", + path: "/oc-if:interfaces/oc-if:interface[oc-if:name=current()/../interface]/oc-if:subinterfaces", + expectOK: true, + want: []interface{}{"oc-if:interfaces", "oc-if:interface", map[string]string{"oc-if:name": "current()/../interface"}, "oc-if:subinterfaces"}, }} for _, test := range tests {