Skip to content

Commit 3eeb3f0

Browse files
gfreyk8s-ci-robot
authored andcommitted
Add map library with merge functionality
Allows to merge two maps. Maps must have identical value types. Keys of the second map take precedence.
1 parent 57d3b38 commit 3eeb3f0

File tree

3 files changed

+208
-0
lines changed

3 files changed

+208
-0
lines changed

pkg/cel/environment.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ func BaseDeclarations() []cel.EnvOption {
105105
k8scellib.URLs(),
106106
k8scellib.Regex(),
107107
library.Random(),
108+
library.Maps(),
108109
}
109110
}
110111

pkg/cel/library/maps.go

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package library
16+
17+
import (
18+
"math"
19+
20+
"github.com/google/cel-go/cel"
21+
"github.com/google/cel-go/common/types"
22+
"github.com/google/cel-go/common/types/ref"
23+
"github.com/google/cel-go/common/types/traits"
24+
)
25+
26+
// Maps returns a cel.EnvOption to configure extended functions for map manipulation.
27+
//
28+
// # Merge
29+
//
30+
// Merges two maps. Keys from the second map overwrite already available keys in the first map.
31+
// Keys must be of type string, value types must be identical in the maps merged.
32+
//
33+
// map(string, T).merge(map(string, T)) -> map(string, T)
34+
//
35+
// Examples:
36+
//
37+
// {}.merge({}) == {}
38+
// {}.merge({'a': 1}) == {'a': 1}`},
39+
// {}.merge({'a': 2.1}) == {'a': 2.1}`},
40+
// {}.merge({'a': 'foo'}) == {'a': 'foo'}`},
41+
// {'a': 1}.merge({}) == {'a': 1}`},
42+
// {'a': 1}.merge({'b': 2}) == {'a': 1, 'b': 2}`},
43+
// {'a': 1}.merge({'a': 2, 'b': 2}) == {'a': 2, 'b': 2}`},
44+
func Maps(options ...MapsOption) cel.EnvOption {
45+
l := &mapsLib{version: math.MaxUint32}
46+
for _, o := range options {
47+
l = o(l)
48+
}
49+
return cel.Lib(l)
50+
}
51+
52+
type mapsLib struct {
53+
version uint32
54+
}
55+
56+
type MapsOption func(*mapsLib) *mapsLib
57+
58+
// MapsVersion configures the version of the maps library.
59+
//
60+
// The version limits which functions are available. Only functions introduced
61+
// below or equal to the given version included in the library. If this option
62+
// is not set, all functions are available.
63+
//
64+
// See the library documentation to determine which version a function was introduced.
65+
// If the documentation does not state which version a function was introduced, it can
66+
// be assumed to be introduced at version 0, when the library was first created.
67+
func MapsVersion(version uint32) MapsOption {
68+
return func(lib *mapsLib) *mapsLib {
69+
lib.version = version
70+
return lib
71+
}
72+
}
73+
74+
// LibraryName implements the cel.SingletonLibrary interface method.
75+
func (mapsLib) LibraryName() string {
76+
return "cel.lib.ext.maps"
77+
}
78+
79+
// CompileOptions implements the cel.Library interface method.
80+
func (lib mapsLib) CompileOptions() []cel.EnvOption {
81+
mapType := cel.MapType(cel.TypeParamType("K"), cel.TypeParamType("V"))
82+
opts := []cel.EnvOption{
83+
cel.Function("merge",
84+
cel.MemberOverload("map_merge",
85+
[]*cel.Type{mapType, mapType},
86+
mapType,
87+
cel.BinaryBinding(mergeVals),
88+
),
89+
),
90+
}
91+
return opts
92+
}
93+
94+
// ProgramOptions implements the cel.Library interface method.
95+
func (lib mapsLib) ProgramOptions() []cel.ProgramOption {
96+
return []cel.ProgramOption{}
97+
}
98+
99+
func mergeVals(lhs, rhs ref.Val) ref.Val {
100+
left, lok := lhs.(traits.Mapper)
101+
right, rok := rhs.(traits.Mapper)
102+
if !lok || !rok {
103+
return types.ValOrErr(lhs, "no such overload: %v.merge(%v)", lhs.Type(), rhs.Type())
104+
}
105+
return merge(left, right)
106+
}
107+
108+
// merge returns a new map containing entries from both maps.
109+
// Keys in 'other' overwrite keys in 'self'.
110+
func merge(self, other traits.Mapper) traits.Mapper {
111+
result := mapperTraitToMutableMapper(other)
112+
for i := self.Iterator(); i.HasNext().(types.Bool); {
113+
k := i.Next()
114+
if !result.Contains(k).(types.Bool) {
115+
result.Insert(k, self.Get(k))
116+
}
117+
}
118+
return result.ToImmutableMap()
119+
}
120+
121+
// mapperTraitToMutableMapper copies a traits.Mapper into a MutableMap.
122+
func mapperTraitToMutableMapper(m traits.Mapper) traits.MutableMapper {
123+
vals := make(map[ref.Val]ref.Val, m.Size().(types.Int))
124+
for it := m.Iterator(); it.HasNext().(types.Bool); {
125+
k := it.Next()
126+
vals[k] = m.Get(k)
127+
}
128+
return types.NewMutableMap(types.DefaultTypeAdapter, vals)
129+
}

pkg/cel/library/maps_test.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package library
16+
17+
import (
18+
"fmt"
19+
"testing"
20+
21+
"github.com/google/cel-go/cel"
22+
)
23+
24+
func TestMaps(t *testing.T) {
25+
mapsTests := []struct {
26+
expr string
27+
}{
28+
{expr: `{}.merge({}) == {}`},
29+
{expr: `{}.merge({'a': 1}) == {'a': 1}`},
30+
{expr: `{}.merge({'a': 2.1}) == {'a': 2.1}`},
31+
{expr: `{}.merge({'a': 'foo'}) == {'a': 'foo'}`},
32+
{expr: `{'a': 1}.merge({}) == {'a': 1}`},
33+
{expr: `{'a': 1}.merge({'b': 2}) == {'a': 1, 'b': 2}`},
34+
{expr: `{'a': 1}.merge({'a': 2, 'b': 2}) == {'a': 2, 'b': 2}`},
35+
}
36+
37+
env := testMapsEnv(t)
38+
for i, tc := range mapsTests {
39+
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
40+
var asts []*cel.Ast
41+
pAst, iss := env.Parse(tc.expr)
42+
if iss.Err() != nil {
43+
t.Fatalf("env.Parse(%v) failed: %v", tc.expr, iss.Err())
44+
}
45+
asts = append(asts, pAst)
46+
cAst, iss := env.Check(pAst)
47+
if iss.Err() != nil {
48+
t.Fatalf("env.Check(%v) failed: %v", tc.expr, iss.Err())
49+
}
50+
asts = append(asts, cAst)
51+
52+
for _, ast := range asts {
53+
prg, err := env.Program(ast)
54+
if err != nil {
55+
t.Fatalf("env.Program() failed: %v", err)
56+
}
57+
out, _, err := prg.Eval(cel.NoVars())
58+
if err != nil {
59+
t.Fatal(err)
60+
} else if out.Value() != true {
61+
t.Errorf("got %v, wanted true for expr: %s", out.Value(), tc.expr)
62+
}
63+
}
64+
})
65+
}
66+
}
67+
68+
func testMapsEnv(t *testing.T, opts ...cel.EnvOption) *cel.Env {
69+
t.Helper()
70+
baseOpts := []cel.EnvOption{
71+
Maps(),
72+
}
73+
env, err := cel.NewEnv(append(baseOpts, opts...)...)
74+
if err != nil {
75+
t.Fatalf("cel.NewEnv(Maps()) failed: %v", err)
76+
}
77+
return env
78+
}

0 commit comments

Comments
 (0)