Skip to content

Commit 9ded8ad

Browse files
authored
feat: support reference var in rule files (#97)
1 parent 1e1888d commit 9ded8ad

File tree

2 files changed

+189
-0
lines changed

2 files changed

+189
-0
lines changed

pkg/rule_engine/action.go

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,12 @@ func NewAction(
7070
return nil, errors.Errorf("failed to compile argument %s: %v", k, err)
7171
}
7272
kwargs[k] = evaluator
73+
case []string:
74+
evaluator, err := compileArrayExpr(env, val)
75+
if err != nil {
76+
return nil, errors.Errorf("failed to compile array argument %s: %v", k, err)
77+
}
78+
kwargs[k] = evaluator
7379
case map[string]interface{}:
7480
evaluator, err := compileDictExpr(env, val)
7581
if err != nil {
@@ -203,6 +209,85 @@ func compileDictExpr(env *cel.Env, dict map[string]interface{}) (expressionEvalu
203209
}, nil
204210
}
205211

212+
// compileArrayExpr compiles expressions in a string array.
213+
// Each array element can contain embedded CEL expressions like "{scope.files}".
214+
func compileArrayExpr(env *cel.Env, arr []string) (expressionEvaluator, error) {
215+
compiledEvaluators := make([]expressionEvaluator, len(arr))
216+
217+
for i, element := range arr {
218+
pattern := regexp.MustCompile(`\{\s*(.*?)\s*}`)
219+
matches := pattern.FindAllStringSubmatch(element, -1)
220+
221+
switch {
222+
case len(matches) == 0:
223+
// If no expressions found, return the constant string
224+
compiledEvaluators[i] = func(map[string]interface{}) (interface{}, error) {
225+
return element, nil
226+
}
227+
case len(matches) == 1 && matches[0][0] == element:
228+
// Special case: element is pure expression like "{scope.files}"
229+
// Try to evaluate it as an array expression first
230+
ast, issues := env.Compile(matches[0][1])
231+
if issues != nil && issues.Err() != nil {
232+
return nil, errors.Errorf("failed to compile array expression '%s': %v", matches[0][1], issues.Err())
233+
}
234+
program, err := env.Program(ast)
235+
if err != nil {
236+
return nil, errors.Errorf("failed to create program for array expression '%s': %v", matches[0][1], err)
237+
}
238+
239+
compiledEvaluators[i] = func(activation map[string]interface{}) (interface{}, error) {
240+
val, _, err := program.Eval(activation)
241+
if err != nil {
242+
return nil, errors.Errorf("failed to evaluate array expression '%s': %v", matches[0][1], err)
243+
}
244+
return val.Value(), nil
245+
}
246+
default:
247+
// Element contains mixed content with expressions, treat as string
248+
evaluator, err := compileEmbeddedExpr(env, element)
249+
if err != nil {
250+
return nil, errors.Errorf("failed to compile embedded expression in array element: %v", err)
251+
}
252+
compiledEvaluators[i] = evaluator
253+
}
254+
}
255+
256+
return func(activation map[string]interface{}) (interface{}, error) {
257+
result := make([]string, 0)
258+
259+
for _, evaluator := range compiledEvaluators {
260+
value, err := evaluator(activation)
261+
if err != nil {
262+
return nil, err
263+
}
264+
265+
switch v := value.(type) {
266+
case []interface{}:
267+
// If the expression evaluates to an array, append all elements
268+
for _, item := range v {
269+
result = append(result, fmt.Sprintf("%v", item))
270+
}
271+
case []string:
272+
// If it's already a string array, append directly
273+
result = append(result, v...)
274+
case string:
275+
// Single string value
276+
if v != "" {
277+
result = append(result, v)
278+
}
279+
default:
280+
// Convert other types to string
281+
if str := fmt.Sprintf("%v", v); str != "" {
282+
result = append(result, str)
283+
}
284+
}
285+
}
286+
287+
return result, nil
288+
}, nil
289+
}
290+
206291
func (a *Action) String() string {
207292
return fmt.Sprintf("Action(%s)%v", a.Name, a.RawKwargs)
208293
}

pkg/rule_engine/action_test.go

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,3 +182,107 @@ func TestActionValidation(t *testing.T) {
182182
})
183183
}
184184
}
185+
186+
func TestActionArrayExpansion(t *testing.T) {
187+
t.Parallel()
188+
189+
tests := []struct {
190+
name string
191+
rawKwargs map[string]interface{}
192+
activation map[string]interface{}
193+
expected map[string]interface{}
194+
expectErr bool
195+
}{
196+
{
197+
name: "array with scope.files expansion",
198+
rawKwargs: map[string]interface{}{
199+
"extra_files": []string{"/static/file1.txt", "{scope.files}", "/static/file2.txt"},
200+
},
201+
activation: map[string]interface{}{
202+
"scope": map[string]interface{}{
203+
"files": []string{"/dynamic/file1.txt", "/dynamic/file2.txt"},
204+
},
205+
},
206+
expected: map[string]interface{}{
207+
"extra_files": []string{"/static/file1.txt", "/dynamic/file1.txt", "/dynamic/file2.txt", "/static/file2.txt"},
208+
},
209+
expectErr: false,
210+
},
211+
{
212+
name: "array with mixed expressions",
213+
rawKwargs: map[string]interface{}{
214+
"files": []string{"{scope.code}_prefix.txt", "{scope.files}", "suffix_{scope.version}.log"},
215+
},
216+
activation: map[string]interface{}{
217+
"scope": map[string]interface{}{
218+
"code": "ABC123",
219+
"version": "v1.0",
220+
"files": []string{"data1.bin", "data2.bin"},
221+
},
222+
},
223+
expected: map[string]interface{}{
224+
"files": []string{"ABC123_prefix.txt", "data1.bin", "data2.bin", "suffix_v1.0.log"},
225+
},
226+
expectErr: false,
227+
},
228+
{
229+
name: "empty array expansion",
230+
rawKwargs: map[string]interface{}{
231+
"files": []string{"{scope.files}"},
232+
},
233+
activation: map[string]interface{}{
234+
"scope": map[string]interface{}{
235+
"files": []string{},
236+
},
237+
},
238+
expected: map[string]interface{}{
239+
"files": []string{},
240+
},
241+
expectErr: false,
242+
},
243+
{
244+
name: "no array expansion - static files only",
245+
rawKwargs: map[string]interface{}{
246+
"files": []string{"/path/file1.txt", "/path/file2.txt"},
247+
},
248+
activation: map[string]interface{}{
249+
"scope": map[string]interface{}{},
250+
},
251+
expected: map[string]interface{}{
252+
"files": []string{"/path/file1.txt", "/path/file2.txt"},
253+
},
254+
expectErr: false,
255+
},
256+
}
257+
258+
for _, test := range tests {
259+
t.Run(test.name, func(t *testing.T) {
260+
t.Parallel()
261+
262+
var result map[string]interface{}
263+
action, err := createTestAction(test.rawKwargs, &result)
264+
if err != nil {
265+
if !test.expectErr {
266+
t.Fatalf("Failed to create action: %v", err)
267+
}
268+
return
269+
}
270+
271+
err = action.RunDirect(test.activation)
272+
if test.expectErr {
273+
if err == nil {
274+
t.Error("Expected error but got none")
275+
}
276+
return
277+
}
278+
279+
if err != nil {
280+
t.Fatalf("Action execution failed: %v", err)
281+
}
282+
283+
if !reflect.DeepEqual(result["extra_files"], test.expected["extra_files"]) {
284+
t.Errorf("Expected %v, got %v", test.expected, result)
285+
}
286+
})
287+
}
288+
}

0 commit comments

Comments
 (0)