Skip to content

Commit 84d0d45

Browse files
authored
Merge pull request #882 from kathleenfrench/kfrench/cel-eval-string
support string evaluation in CEL expressions
2 parents 8b1f852 + 567773d commit 84d0d45

File tree

2 files changed

+153
-0
lines changed

2 files changed

+153
-0
lines changed

runtime/cel/expression.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,3 +136,16 @@ func (e *Expression) EvaluateBoolean(ctx context.Context, data map[string]any) (
136136
}
137137
return bool(result), nil
138138
}
139+
140+
// EvaluateString evaluates the expression with the given data and returns the result as a string.
141+
func (e *Expression) EvaluateString(ctx context.Context, data map[string]any) (string, error) {
142+
val, _, err := e.prog.ContextEval(ctx, data)
143+
if err != nil {
144+
return "", fmt.Errorf("failed to evaluate the CEL expression '%s': %w", e.expr, err)
145+
}
146+
result, ok := val.(types.String)
147+
if !ok {
148+
return "", fmt.Errorf("failed to evaluate CEL expression as string: '%s'", e.expr)
149+
}
150+
return string(result), nil
151+
}

runtime/cel/expression_test.go

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,3 +219,143 @@ func TestExpression_EvaluateBoolean(t *testing.T) {
219219
})
220220
}
221221
}
222+
223+
func TestExpression_EvaluateString(t *testing.T) {
224+
for _, tt := range []struct {
225+
name string
226+
expr string
227+
opts []cel.Option
228+
data map[string]any
229+
result string
230+
err string
231+
}{
232+
{
233+
name: "non-existent field",
234+
expr: "foo",
235+
data: map[string]any{},
236+
err: "failed to evaluate the CEL expression 'foo': no such attribute(s): foo",
237+
},
238+
{
239+
name: "string field",
240+
expr: "foo",
241+
data: map[string]any{"foo": "some-value"},
242+
result: "some-value",
243+
},
244+
{
245+
name: "non-string field",
246+
expr: "foo",
247+
data: map[string]any{"foo": 123},
248+
err: "failed to evaluate CEL expression as string: 'foo'",
249+
},
250+
{
251+
name: "nested string field",
252+
expr: "foo.bar",
253+
data: map[string]any{"foo": map[string]any{"bar": "some-value"}},
254+
result: "some-value",
255+
},
256+
{
257+
name: "compiled expression returning string",
258+
expr: "foo.bar",
259+
opts: []cel.Option{cel.WithCompile(), cel.WithStructVariables("foo")},
260+
data: map[string]any{"foo": map[string]any{"bar": "some-value"}},
261+
result: "some-value",
262+
},
263+
{
264+
name: "compiled expression returning string multiple variables",
265+
expr: "foo.bar + '/' + foo.baz + '/' + bar.biz",
266+
opts: []cel.Option{
267+
cel.WithCompile(),
268+
cel.WithStructVariables("foo", "bar"),
269+
},
270+
data: map[string]any{
271+
"foo": map[string]any{
272+
"bar": "some-value",
273+
"baz": "some-other-value"},
274+
"bar": map[string]any{
275+
"biz": "some-third-value",
276+
},
277+
},
278+
result: "some-value/some-other-value/some-third-value",
279+
},
280+
{
281+
name: "compiled expression with string manipulation and zero index",
282+
expr: "foo.bar + '/' + foo.baz + '/' + bar.uid.split('-')[0].lowerAscii()",
283+
opts: []cel.Option{
284+
cel.WithCompile(),
285+
cel.WithStructVariables("foo", "bar"),
286+
},
287+
data: map[string]any{
288+
"foo": map[string]any{
289+
"bar": "some-value",
290+
"baz": "some-other-value"},
291+
"bar": map[string]any{
292+
"uid": "AKS2J23-DAFLSDD-123J5LS",
293+
},
294+
},
295+
result: "some-value/some-other-value/aks2j23",
296+
},
297+
{
298+
name: "compiled expression with string manipulation and first",
299+
expr: "foo.bar + '/' + foo.baz + '/' + bar.uid.split('-').first().value().lowerAscii()",
300+
opts: []cel.Option{
301+
cel.WithCompile(),
302+
cel.WithStructVariables("foo", "bar"),
303+
},
304+
data: map[string]any{
305+
"foo": map[string]any{
306+
"bar": "some-value",
307+
"baz": "some-other-value"},
308+
"bar": map[string]any{
309+
"uid": "AKS2J23-DAFLSDD-123J5LS",
310+
},
311+
},
312+
result: "some-value/some-other-value/aks2j23",
313+
},
314+
{
315+
name: "compiled expression with first",
316+
expr: "foo.bar.split('-').first().value()",
317+
opts: []cel.Option{cel.WithCompile(), cel.WithStructVariables("foo")},
318+
data: map[string]any{
319+
"foo": map[string]any{"bar": "hello-world-testing-123"},
320+
},
321+
result: "hello",
322+
},
323+
{
324+
name: "compiled expression with last",
325+
expr: "foo.bar.split('-').last().value()",
326+
opts: []cel.Option{cel.WithCompile(), cel.WithStructVariables("foo")},
327+
data: map[string]any{
328+
"foo": map[string]any{"bar": "hello-world-testing-123"},
329+
},
330+
result: "123",
331+
},
332+
{
333+
name: "error without value method",
334+
expr: "foo.bar.split('-').first()",
335+
opts: []cel.Option{cel.WithCompile(), cel.WithStructVariables("foo")},
336+
data: map[string]any{
337+
"foo": map[string]any{"bar": "hello-world-testing-123"},
338+
},
339+
err: "failed to evaluate CEL expression as string: 'foo.bar.split('-').first()'",
340+
},
341+
} {
342+
t.Run(tt.name, func(t *testing.T) {
343+
t.Parallel()
344+
345+
g := NewWithT(t)
346+
347+
e, err := cel.NewExpression(tt.expr, tt.opts...)
348+
g.Expect(err).NotTo(HaveOccurred())
349+
350+
result, err := e.EvaluateString(context.Background(), tt.data)
351+
352+
if tt.err != "" {
353+
g.Expect(err).To(HaveOccurred())
354+
g.Expect(err.Error()).To(ContainSubstring(tt.err))
355+
} else {
356+
g.Expect(err).NotTo(HaveOccurred())
357+
g.Expect(result).To(Equal(tt.result))
358+
}
359+
})
360+
}
361+
}

0 commit comments

Comments
 (0)