diff --git a/docs/cel_expressions.md b/docs/cel_expressions.md index bd61e82f3..0cc2f9a71 100644 --- a/docs/cel_expressions.md +++ b/docs/cel_expressions.md @@ -442,6 +442,35 @@ which can be accessed by indexing.
{"testing":"value"}.marshalJSON() == "{\"testing\": \"value\"}"
+ + + first() + + +
<jsonArray>.first() -> <jsonObject>
+ + + Returns the first element in the array. + + +
[1, 2, 3, 4, 5].first() == 1
+ + + + + last() + + +
<jsonArray>.last() -> <jsonObject>
+ + + Returns the last element in the array. + + +
[1, 2, 3, 4, 5].last() == 5
+ + + ## Troubleshooting CEL expressions diff --git a/pkg/interceptors/cel/cel_test.go b/pkg/interceptors/cel/cel_test.go index 7b48ceef1..482fe5f10 100644 --- a/pkg/interceptors/cel/cel_test.go +++ b/pkg/interceptors/cel/cel_test.go @@ -440,6 +440,10 @@ func TestExpressionEvaluation(t *testing.T) { }, }, }, + "numbers": []int64{ + 1, 2, 3, 4, 5, + }, + "emptyList": []int64{}, } refParts := strings.Split(testRef, "/") @@ -621,6 +625,47 @@ func TestExpressionEvaluation(t *testing.T) { expr: "body.jsonArray.join(', ')", want: types.String("one, two"), }, + { + name: "last element in array", + expr: "body.testURL.parseURL().path.split('/').last()", + want: types.String("to"), + }, + { + name: "last element in empty array", + expr: `body.emptyList.last()`, + want: types.NullValue, + }, + { + name: "last element in numeric array", + expr: `body.numbers.last()`, + want: types.Int(5), + }, + { + name: "last element in literal array", + expr: "[1, 2, 3, 4, 5].last()", + want: types.Int(5), + }, + { + name: "first element in literal array", + expr: "[1, 2, 3, 4, 5].first()", + want: types.Int(1), + }, + { + name: "first element in array", + // Splitting gets an empty route element so this filters it out. + expr: "body.testURL.parseURL().path.split('/').filter(t, t.size() > 0).first()", + want: types.String("path"), + }, + { + name: "first element in empty array", + expr: `body.emptyList.first()`, + want: types.NullValue, + }, + { + name: "first element in numeric array", + expr: `body.numbers.first()`, + want: types.Int(1), + }, } for _, tt := range tests { t.Run(tt.name, func(rt *testing.T) { diff --git a/pkg/interceptors/cel/triggers.go b/pkg/interceptors/cel/triggers.go index f1d95fd91..9b3736371 100644 --- a/pkg/interceptors/cel/triggers.go +++ b/pkg/interceptors/cel/triggers.go @@ -27,6 +27,7 @@ import ( "github.com/google/cel-go/cel" "github.com/google/cel-go/common/types" "github.com/google/cel-go/common/types/ref" + "github.com/google/cel-go/common/types/traits" "github.com/google/cel-go/interpreter/functions" "github.com/tektoncd/triggers/pkg/interceptors" "sigs.k8s.io/yaml" @@ -187,6 +188,12 @@ func (t triggersLib) CompileOptions() []cel.EnvOption { cel.UnaryBinding(marshalJSON)), cel.MemberOverload("marshalJSON_list", []*cel.Type{listStrDyn}, cel.StringType, cel.UnaryBinding(marshalJSON))), + cel.Function("last", + cel.MemberOverload("last_list", []*cel.Type{listStrDyn}, cel.DynType, + cel.UnaryBinding(listLast))), + cel.Function("first", + cel.MemberOverload("first_list", []*cel.Type{listStrDyn}, cel.DynType, + cel.UnaryBinding(listFirst))), } } @@ -324,6 +331,28 @@ func marshalJSON(val ref.Val) ref.Val { return types.String(marshaledVal) } +func listLast(val ref.Val) ref.Val { + l := val.(traits.Lister) + sz := l.Size().Value().(int64) + + if sz == 0 { + return types.NullValue + } + + return l.Get(types.Int(sz - 1)) +} + +func listFirst(val ref.Val) ref.Val { + l := val.(traits.Lister) + sz := l.Size().Value().(int64) + + if sz == 0 { + return types.NullValue + } + + return l.Get(types.Int(0)) +} + func max(x, y types.Int) types.Int { switch x.Compare(y) { case types.IntNegOne: