From ca266f7a4b25a1b642367e362bf18e65dfde0245 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 9 Apr 2026 11:19:06 +0000
Subject: [PATCH 1/3] feat: add xpath(xpathStr, xmlStr) CEL function
Agent-Logs-Url: https://github.com/flanksource/gomplate/sessions/794901fb-1d08-4aa0-aa93-e5c15420c765
Co-authored-by: moshloop <1489660+moshloop@users.noreply.github.com>
---
coll/json.go | 30 +++++++++++++++++++
funcs/cel_exports.go | 1 +
funcs/coll.go | 1 +
funcs/coll_gen.go | 16 ++++++++++
go.mod | 3 ++
go.sum | 71 ++++++++++++++++++++++++++++++++++++++++++++
tests/cel_test.go | 5 ++++
7 files changed, 127 insertions(+)
diff --git a/coll/json.go b/coll/json.go
index 45338fba2..7fbea7536 100644
--- a/coll/json.go
+++ b/coll/json.go
@@ -5,7 +5,9 @@ import (
"encoding/json"
"fmt"
"reflect"
+ "strings"
+ "github.com/antchfx/xmlquery"
"github.com/itchyny/gojq"
"github.com/jmespath/go-jmespath"
"github.com/ohler55/ojg/jp"
@@ -113,6 +115,34 @@ func JSONPath(jsonPath string, in interface{}) (interface{}, error) {
return out, nil
}
+// XPath evaluates an XPath expression against an XML string and returns the
+// result. If the expression matches a single node its text content is returned;
+// if it matches multiple nodes a []string of text contents is returned.
+func XPath(xpathStr, xmlStr string) (interface{}, error) {
+ doc, err := xmlquery.Parse(strings.NewReader(xmlStr))
+ if err != nil {
+ return nil, fmt.Errorf("xpath: parsing xml: %w", err)
+ }
+
+ nodes, err := xmlquery.QueryAll(doc, xpathStr)
+ if err != nil {
+ return nil, fmt.Errorf("xpath: evaluating expression %q: %w", xpathStr, err)
+ }
+
+ switch len(nodes) {
+ case 0:
+ return "", nil
+ case 1:
+ return nodes[0].InnerText(), nil
+ default:
+ out := make([]string, len(nodes))
+ for i, n := range nodes {
+ out[i] = n.InnerText()
+ }
+ return out, nil
+ }
+}
+
func isSupportableType(in interface{}) bool {
switch in.(type) {
case map[string]interface{},
diff --git a/funcs/cel_exports.go b/funcs/cel_exports.go
index 5781dfe8d..d41224465 100644
--- a/funcs/cel_exports.go
+++ b/funcs/cel_exports.go
@@ -26,6 +26,7 @@ var CelEnvOption = []cel.EnvOption{
collMapToKeyValGen,
collJmesPathGen,
collJsonPathGen,
+ collXPathGen,
celLabelsMatch,
celCoalesce,
celFirst,
diff --git a/funcs/coll.go b/funcs/coll.go
index 7eac0ef6f..1a26fef28 100644
--- a/funcs/coll.go
+++ b/funcs/coll.go
@@ -59,6 +59,7 @@ func CreateCollFuncs(ctx context.Context) map[string]interface{} {
f["keyValToMap"] = coll.KeyValToMap
f["jsonpath"] = coll.JSONPath
f["jmespath"] = coll.JMESPath
+ f["xpath"] = coll.XPath
return f
}
diff --git a/funcs/coll_gen.go b/funcs/coll_gen.go
index 79e934452..5e13bed20 100644
--- a/funcs/coll_gen.go
+++ b/funcs/coll_gen.go
@@ -354,6 +354,22 @@ var collJmesPathGen = cel.Function("jmespath",
+var collXPathGen = cel.Function("xpath",
+ cel.Overload("xpath_string_string",
+ []*cel.Type{
+ cel.StringType, cel.StringType,
+ },
+ cel.DynType,
+ cel.BinaryBinding(func(xpathExpr, xmlStr ref.Val) ref.Val {
+ result, err := coll.XPath(xpathExpr.Value().(string), xmlStr.Value().(string))
+ if err != nil {
+ return types.WrapErr(err)
+ }
+ return types.DefaultTypeAdapter.NativeToValue(result)
+ }),
+ ),
+)
+
var collPickGen = cel.Function("pick",
cel.MemberOverload("pick_interface{}",
[]*cel.Type{
diff --git a/go.mod b/go.mod
index b8053803f..c774483ac 100644
--- a/go.mod
+++ b/go.mod
@@ -42,6 +42,8 @@ require github.com/hairyhenderson/yaml v0.0.0-20220618171115-2d35fca545ce
require (
cel.dev/expr v0.25.1 // indirect
+ github.com/antchfx/xmlquery v1.5.1 // indirect
+ github.com/antchfx/xpath v1.3.6 // indirect
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bsm/gomega v1.27.10 // indirect
@@ -58,6 +60,7 @@ require (
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/goccy/go-yaml v1.19.2 // indirect
+ github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 // indirect
github.com/gosimple/unidecode v1.0.1 // indirect
github.com/hexops/gotextdiff v1.0.3 // indirect
diff --git a/go.sum b/go.sum
index d813b38db..7a3cc2850 100644
--- a/go.sum
+++ b/go.sum
@@ -4,6 +4,10 @@ github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJ
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
+github.com/antchfx/xmlquery v1.5.1 h1:T9I4Ns1EXiWHy0IqKupGhnfTQtJwlGrpXtauYOoNv78=
+github.com/antchfx/xmlquery v1.5.1/go.mod h1:bVqnl7TaDXSReKINrhZz+2E/PbCu2tUahb+wZ7WZNT8=
+github.com/antchfx/xpath v1.3.6 h1:s0y+ElRRtTQdfHP609qFu0+c6bglDv20pqOViQjjdPI=
+github.com/antchfx/xpath v1.3.6/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ=
github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
@@ -56,8 +60,11 @@ github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
+github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
+github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/google/cel-go v0.27.0 h1:e7ih85+4qVrBuqQWTW4FKSqZYokVuc3HnhH5keboFTo=
github.com/google/cel-go v0.27.0/go.mod h1:tTJ11FWqnhw5KKpnWpvW9CJC3Y9GK4EIS0WXnBbebzw=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
@@ -184,6 +191,7 @@ github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
@@ -204,21 +212,84 @@ go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
+golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
+golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
+golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0=
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
+golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
+golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
+golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
+golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
+golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
+golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
+golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
+golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
+golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
+golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
+golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
+golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
+golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
+golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto/googleapis/api v0.0.0-20260223185530-2f722ef697dc h1:ULD+ToGXUIU6Pkzr1ARxdyvwfHbelw+agoFDRbLg4TU=
google.golang.org/genproto/googleapis/api v0.0.0-20260223185530-2f722ef697dc/go.mod h1:M5krXqk4GhBKvB596udGL3UyjL4I1+cTbK0orROM9ng=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260223185530-2f722ef697dc h1:51Wupg8spF+5FC6D+iMKbOddFjMckETnNnEiZ+HX37s=
diff --git a/tests/cel_test.go b/tests/cel_test.go
index 878a6e841..47e78964a 100644
--- a/tests/cel_test.go
+++ b/tests/cel_test.go
@@ -279,6 +279,11 @@ func TestCelJSON(t *testing.T) {
{map[string]interface{}{"i": person}, "jq('.Address.city_name', i)", "Kathmandu"},
{map[string]interface{}{"i": person}, "jq('.Address.country', i)", ""},
{map[string]interface{}{"i": personJSONString}, "jq('.Address.city_name', i)", "Kathmandu"},
+
+ // XPath
+ {nil, `xpath('//name/text()', 'Alice')`, "Alice"},
+ {nil, `xpath('//item/@id', ' ')`, "42"},
+ {nil, `xpath('//missing', 'Alice')`, ""},
})
}
From 3d59667189911bcbff540a2a166a14d6e684ae0e Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 9 Apr 2026 11:23:36 +0000
Subject: [PATCH 2/3] feat: add xpath(xpathStr, xmlStr) CEL function
Agent-Logs-Url: https://github.com/flanksource/gomplate/sessions/794901fb-1d08-4aa0-aa93-e5c15420c765
Co-authored-by: moshloop <1489660+moshloop@users.noreply.github.com>
---
go.mod | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/go.mod b/go.mod
index c774483ac..483de6912 100644
--- a/go.mod
+++ b/go.mod
@@ -5,6 +5,7 @@ go 1.25.1
require (
github.com/Masterminds/goutils v1.1.1
github.com/Masterminds/semver/v3 v3.4.0
+ github.com/antchfx/xmlquery v1.5.1
github.com/flanksource/commons v1.47.2
github.com/flanksource/is-healthy v1.0.86
github.com/flanksource/kubectl-neat v1.0.4
@@ -42,7 +43,6 @@ require github.com/hairyhenderson/yaml v0.0.0-20220618171115-2d35fca545ce
require (
cel.dev/expr v0.25.1 // indirect
- github.com/antchfx/xmlquery v1.5.1 // indirect
github.com/antchfx/xpath v1.3.6 // indirect
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect
From 58327cece1e630e79933f7be824190f82cb766d5 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 9 Apr 2026 15:22:15 +0000
Subject: [PATCH 3/3] fix: xpath always returns a list; use .first() for single
value
Agent-Logs-Url: https://github.com/flanksource/gomplate/sessions/6eec3c3f-8769-4879-9e2d-202d09fe0baf
Co-authored-by: moshloop <1489660+moshloop@users.noreply.github.com>
---
coll/json.go | 23 ++++++++---------------
tests/cel_test.go | 9 +++++----
2 files changed, 13 insertions(+), 19 deletions(-)
diff --git a/coll/json.go b/coll/json.go
index 7fbea7536..48b4ca7a5 100644
--- a/coll/json.go
+++ b/coll/json.go
@@ -115,10 +115,10 @@ func JSONPath(jsonPath string, in interface{}) (interface{}, error) {
return out, nil
}
-// XPath evaluates an XPath expression against an XML string and returns the
-// result. If the expression matches a single node its text content is returned;
-// if it matches multiple nodes a []string of text contents is returned.
-func XPath(xpathStr, xmlStr string) (interface{}, error) {
+// XPath evaluates an XPath expression against an XML string and returns a
+// list of text contents of all matching nodes. Use .first() on the result to
+// get a single value.
+func XPath(xpathStr, xmlStr string) ([]string, error) {
doc, err := xmlquery.Parse(strings.NewReader(xmlStr))
if err != nil {
return nil, fmt.Errorf("xpath: parsing xml: %w", err)
@@ -129,18 +129,11 @@ func XPath(xpathStr, xmlStr string) (interface{}, error) {
return nil, fmt.Errorf("xpath: evaluating expression %q: %w", xpathStr, err)
}
- switch len(nodes) {
- case 0:
- return "", nil
- case 1:
- return nodes[0].InnerText(), nil
- default:
- out := make([]string, len(nodes))
- for i, n := range nodes {
- out[i] = n.InnerText()
- }
- return out, nil
+ out := make([]string, len(nodes))
+ for i, n := range nodes {
+ out[i] = n.InnerText()
}
+ return out, nil
}
func isSupportableType(in interface{}) bool {
diff --git a/tests/cel_test.go b/tests/cel_test.go
index 47e78964a..7aa3d69b4 100644
--- a/tests/cel_test.go
+++ b/tests/cel_test.go
@@ -280,10 +280,11 @@ func TestCelJSON(t *testing.T) {
{map[string]interface{}{"i": person}, "jq('.Address.country', i)", ""},
{map[string]interface{}{"i": personJSONString}, "jq('.Address.city_name', i)", "Kathmandu"},
- // XPath
- {nil, `xpath('//name/text()', 'Alice')`, "Alice"},
- {nil, `xpath('//item/@id', ' ')`, "42"},
- {nil, `xpath('//missing', 'Alice')`, ""},
+ // XPath always returns a list; use .first() for a single value
+ {nil, `xpath('//name/text()', 'Alice').first()`, "Alice"},
+ {nil, `xpath('//item/@id', ' ').first()`, "42"},
+ {nil, `xpath('//item/@id', ' ')`, "[1 2]"},
+ {nil, `xpath('//missing', 'Alice')`, "[]"},
})
}