-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathfilter.go
More file actions
339 lines (280 loc) · 9.22 KB
/
Copy pathfilter.go
File metadata and controls
339 lines (280 loc) · 9.22 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
package api
import (
"fmt"
"reflect"
"strings"
"github.com/flanksource/commons/logger"
"github.com/flanksource/gomplate/v3"
"github.com/google/cel-go/cel"
"github.com/google/cel-go/common/types/ref"
)
// boolVal is a simple wrapper for boolean CEL values
type boolVal struct {
value bool
}
func (b *boolVal) Value() interface{} {
return b.value
}
func (b *boolVal) Type() ref.Type {
return nil
}
func (b *boolVal) ConvertToNative(typeDesc reflect.Type) (interface{}, error) {
return b.value, nil
}
func (b *boolVal) ConvertToType(typeValue ref.Type) ref.Val {
return b
}
func (b *boolVal) Equal(other ref.Val) ref.Val {
return b
}
// FilterTableRows filters table rows using a CEL expression.
// Field values are injected directly into the CEL context (no "row." prefix).
// Returns filtered rows or error if CEL expression is invalid.
func FilterTableRows(rows []PrettyDataRow, filterExpr string) ([]PrettyDataRow, error) {
if filterExpr == "" {
return rows, nil
}
if len(rows) == 0 {
return rows, nil
}
// Get variable declarations from the first row
variableDecls := getVariableDeclarationsFromRow(rows[0])
// Create CEL environment with dynamic variables
env, err := createCELEnvironment(variableDecls)
if err != nil {
return nil, fmt.Errorf("failed to create CEL environment: %w", err)
}
// Compile expression once
ast, issues := env.Compile(filterExpr)
if issues != nil && issues.Err() != nil {
return nil, fmt.Errorf("failed to compile CEL expression '%s': %w", filterExpr, issues.Err())
}
prg, err := env.Program(ast)
if err != nil {
return nil, fmt.Errorf("failed to create CEL program: %w", err)
}
filtered := make([]PrettyDataRow, 0, len(rows))
for i, row := range rows {
variables := rowToCELMap(row)
out, _, err := prg.Eval(variables)
if err != nil {
return nil, fmt.Errorf("failed to evaluate filter expression at row %d: %w", i, err)
}
if boolResult, ok := out.Value().(bool); ok && boolResult {
filtered = append(filtered, row)
}
}
logger.V(4).Infof("Filtered %d rows to %d using expression: %s", len(rows), len(filtered), filterExpr)
return filtered, nil
}
// FilterTreeNode recursively filters tree nodes using a CEL expression.
// Field values from node content are injected directly into the CEL context.
// Returns filtered tree or error if CEL expression is invalid.
func FilterTreeNode(node TreeNode, filterExpr string) (TreeNode, error) {
if filterExpr == "" || node == nil {
return node, nil
}
// Collect all unique variable declarations from the entire tree
variableDecls := collectTreeVariableDeclarations(node)
// Create CEL environment with dynamic variables
env, err := createCELEnvironment(variableDecls)
if err != nil {
return nil, fmt.Errorf("failed to create CEL environment: %w", err)
}
// Compile expression once
ast, issues := env.Compile(filterExpr)
if issues != nil && issues.Err() != nil {
return nil, fmt.Errorf("failed to compile CEL expression '%s': %w", filterExpr, issues.Err())
}
prg, err := env.Program(ast)
if err != nil {
return nil, fmt.Errorf("failed to create CEL program: %w", err)
}
return filterTreeNodeRecursive(node, prg)
}
// filterTreeNodeRecursive recursively filters tree nodes
func filterTreeNodeRecursive(node TreeNode, prg cel.Program) (TreeNode, error) {
if node == nil {
return nil, nil
}
// Convert node content to CEL variables
variables := nodeToCELMap(node)
// Evaluate filter for this node
out, _, err := prg.Eval(variables)
if err != nil {
// If evaluation fails due to missing attribute, treat as non-match
// This can happen when a node doesn't have metadata fields that other nodes have
if strings.Contains(err.Error(), "no such attribute") {
// Node doesn't have the required field - treat as non-match
out = &boolVal{value: false}
} else {
return nil, fmt.Errorf("failed to evaluate filter expression: %w", err)
}
}
match := false
if boolResult, ok := out.Value().(bool); ok {
match = boolResult
}
// If node doesn't match, check children
children := node.GetChildren()
if !match && len(children) == 0 {
// Leaf node doesn't match - exclude it
return nil, nil
}
// Process children recursively
var filteredChildren []TreeNode
if len(children) > 0 {
for _, child := range children {
filteredChild, err := filterTreeNodeRecursive(child, prg)
if err != nil {
return nil, err
}
if filteredChild != nil {
filteredChildren = append(filteredChildren, filteredChild)
}
}
}
// If node doesn't match but has matching children, include it with filtered children
if !match && len(filteredChildren) > 0 {
return &ConcreteBranchNode{
Children: filteredChildren,
}, nil
}
// If node matches, include it with filtered children
if match {
if len(filteredChildren) > 0 {
// Create a new node with filtered children
simple := TreeNodeToSimple(node)
simple.Children = filteredChildren
return simple, nil
}
// Leaf node that matches - return as-is
return node, nil
}
// Node doesn't match and has no matching children
return nil, nil
}
// rowToCELMap converts a PrettyDataRow to a flat map for CEL evaluation.
// Field names become variable names (no "row." prefix).
// Uses Primitive() to extract typed values for accurate CEL comparisons.
func rowToCELMap(row PrettyDataRow) map[string]interface{} {
result := make(map[string]interface{})
for key, fieldValue := range row {
// Use Primitive() to get strongly-typed value
// This ensures CEL expressions work with proper types:
// - int64 for integers
// - float64 for floats
// - bool for booleans
// - string for strings
// - time.Time for dates
result[key] = fieldValue.String()
}
return result
}
// nodeToCELMap converts a TreeNode's Pretty text to CEL variables.
// For SimpleTreeNode, extracts label and metadata fields.
func nodeToCELMap(node TreeNode) map[string]interface{} {
result := make(map[string]interface{})
// Get the Pretty() text content
text := node.Pretty()
result["label"] = text.Content
result["content"] = text.Content // Alias for label
if text.Style != "" {
result["style"] = text.Style
}
// If it's a SimpleTreeNode, extract additional fields
if simple, ok := node.(*SimpleTreeNode); ok {
result["label"] = simple.Label
if simple.Icon != "" {
result["icon"] = simple.Icon
}
if simple.Style != "" {
result["style"] = simple.Style
}
// Include metadata fields at top level for easy access
for key, value := range simple.Metadata {
result[key] = value
}
}
return result
}
// createCELEnvironment creates a CEL environment with dynamic variable declarations
func createCELEnvironment(variableDecls []cel.EnvOption) (*cel.Env, error) {
// Get base gomplate functions
gomplateFuncs := gomplate.GetCelEnv(make(map[string]any))
// Combine gomplate functions with variable declarations
envOptions := append(gomplateFuncs, variableDecls...)
return cel.NewEnv(envOptions...)
}
// getVariableDeclarationsFromRow creates CEL variable declarations from a row's fields
func getVariableDeclarationsFromRow(row PrettyDataRow) []cel.EnvOption {
var decls []cel.EnvOption
for key, fieldValue := range row {
celType := inferCELTypeFromValue(fieldValue.String())
decls = append(decls, cel.Variable(key, celType))
}
return decls
}
// getVariableDeclarationsFromNode creates CEL variable declarations from a tree node
func getVariableDeclarationsFromNode(node TreeNode) []cel.EnvOption {
variables := nodeToCELMap(node)
var decls []cel.EnvOption
for key, value := range variables {
celType := inferCELTypeFromValue(value)
decls = append(decls, cel.Variable(key, celType))
}
return decls
}
// collectTreeVariableDeclarations collects all unique variable names from an entire tree
func collectTreeVariableDeclarations(node TreeNode) []cel.EnvOption {
if node == nil {
return nil
}
// Use a map to track unique variable names and their types
vars := make(map[string]*cel.Type)
// Recursively collect variables from this node and all children
collectTreeVariables(node, vars)
// Convert map to CEL variable declarations
var decls []cel.EnvOption
for name, celType := range vars {
decls = append(decls, cel.Variable(name, celType))
}
return decls
}
// collectTreeVariables recursively collects variable names from a tree node and its children
func collectTreeVariables(node TreeNode, vars map[string]*cel.Type) {
if node == nil {
return
}
// Get variables from this node
nodeVars := nodeToCELMap(node)
for key, value := range nodeVars {
celType := inferCELTypeFromValue(value)
// If variable already exists, use DynType to allow different types across nodes
if existingType, exists := vars[key]; exists && existingType != celType {
vars[key] = cel.DynType
} else {
vars[key] = celType
}
}
// Recursively collect from children
for _, child := range node.GetChildren() {
collectTreeVariables(child, vars)
}
}
// inferCELTypeFromValue infers the CEL type from a Go value
func inferCELTypeFromValue(value interface{}) *cel.Type {
switch value.(type) {
case string:
return cel.StringType
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
return cel.IntType
case float32, float64:
return cel.DoubleType
case bool:
return cel.BoolType
default:
// Use DynType for unknown types (time.Time, etc.)
return cel.DynType
}
}