Skip to content

Commit 89ccc46

Browse files
committed
add utility functions
1 parent 568d3c7 commit 89ccc46

File tree

8 files changed

+409
-38
lines changed

8 files changed

+409
-38
lines changed

pkg/collections/maps/utils.go

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
package maps
22

3-
import "github.com/openmcp-project/controller-utils/pkg/collections/filters"
3+
import (
4+
"k8s.io/utils/ptr"
5+
6+
"github.com/openmcp-project/controller-utils/pkg/collections/filters"
7+
"github.com/openmcp-project/controller-utils/pkg/pairs"
8+
)
49

510
// Filter filters a map by applying a filter function to each key-value pair.
611
// Only the entries for which the filter function returns true are kept in the copy.
@@ -50,3 +55,34 @@ func Intersect[K comparable, V any](source map[K]V, maps ...map[K]V) map[K]V {
5055

5156
return res
5257
}
58+
59+
// MapKeys returns a slice of all keys in the map.
60+
// The order is unspecified.
61+
// The keys are not deep-copied, so changes to them could affect the original map.
62+
func MapKeys[K comparable, V any](m map[K]V) []K {
63+
keys := make([]K, 0, len(m))
64+
for k := range m {
65+
keys = append(keys, k)
66+
}
67+
return keys
68+
}
69+
70+
// MapValues returns a slice of all values in the map.
71+
// The order is unspecified.
72+
// The values are not deep-copied, so changes to them could affect the original map.
73+
func MapValues[K comparable, V any](m map[K]V) []V {
74+
values := make([]V, 0, len(m))
75+
for _, v := range m {
76+
values = append(values, v)
77+
}
78+
return values
79+
}
80+
81+
// GetAny returns an arbitrary key-value pair from the map as a pointer to a pairs.Pair.
82+
// If the map is empty, it returns nil.
83+
func GetAny[K comparable, V any](m map[K]V) *pairs.Pair[K, V] {
84+
for k, v := range m {
85+
return ptr.To(pairs.New(k, v))
86+
}
87+
return nil
88+
}

pkg/collections/maps/utils_test.go

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import (
77
"github.com/openmcp-project/controller-utils/pkg/collections/maps"
88
)
99

10-
var _ = Describe("LinkedIterator Tests", func() {
10+
var _ = Describe("Map Utils Tests", func() {
1111

1212
Context("Merge", func() {
1313

@@ -60,4 +60,56 @@ var _ = Describe("LinkedIterator Tests", func() {
6060

6161
})
6262

63+
Context("MapKeys", func() {
64+
65+
It("should return all keys in the map", func() {
66+
m1 := map[string]string{"foo": "bar", "bar": "baz", "foobar": "foobaz"}
67+
keys := maps.MapKeys(m1)
68+
Expect(keys).To(ConsistOf("foo", "bar", "foobar"))
69+
Expect(len(keys)).To(Equal(3))
70+
})
71+
72+
It("should return an empty slice for an empty or nil map", func() {
73+
var nilMap map[string]string
74+
Expect(maps.MapKeys(nilMap)).To(BeEmpty())
75+
Expect(maps.MapKeys(map[string]string{})).To(BeEmpty())
76+
})
77+
78+
})
79+
80+
Context("MapValues", func() {
81+
82+
It("should return all values in the map", func() {
83+
m1 := map[string]string{"foo": "bar", "bar": "baz", "foobar": "foobaz"}
84+
values := maps.MapValues(m1)
85+
Expect(values).To(ConsistOf("bar", "baz", "foobaz"))
86+
Expect(len(values)).To(Equal(3))
87+
})
88+
89+
It("should return an empty slice for an empty or nil map", func() {
90+
var nilMap map[string]string
91+
Expect(maps.MapValues(nilMap)).To(BeEmpty())
92+
Expect(maps.MapValues(map[string]string{})).To(BeEmpty())
93+
})
94+
95+
})
96+
97+
Context("GetAny", func() {
98+
99+
It("should return a key-value pair from the map", func() {
100+
m1 := map[string]string{"foo": "bar", "bar": "baz", "foobar": "foobaz"}
101+
pair := maps.GetAny(m1)
102+
Expect(pair).ToNot(BeNil())
103+
Expect(pair.Key).To(BeElementOf("foo", "bar", "foobar"))
104+
Expect(m1[pair.Key]).To(Equal(pair.Value))
105+
})
106+
107+
It("should return nil for an empty or nil map", func() {
108+
var nilMap map[string]string
109+
Expect(maps.GetAny(nilMap)).To(BeNil())
110+
Expect(maps.GetAny(map[string]string{})).To(BeNil())
111+
})
112+
113+
})
114+
63115
})

pkg/collections/utils.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package collections
2+
3+
// ProjectSlice takes a slice and a projection function and applies this function to each element of the slice.
4+
// It returns a new slice containing the results of the projection.
5+
// The original slice is not modified.
6+
// If the projection function is nil, it returns nil.
7+
func ProjectSlice[X any, Y any](src []X, project func(X) Y) []Y {
8+
if project == nil {
9+
return nil
10+
}
11+
res := make([]Y, len(src))
12+
for i, src := range src {
13+
res[i] = project(src)
14+
}
15+
return res
16+
}
17+
18+
// ProjectMapToSlice takes a map and a projection function and applies this function to each key-value pair in the map.
19+
// It returns a new slice containing the results of the projection.
20+
// The original map is not modified.
21+
// If the projection function is nil, it returns nil.
22+
func ProjectMapToSlice[K comparable, V any, R any](src map[K]V, project func(K, V) R) []R {
23+
if project == nil {
24+
return nil
25+
}
26+
res := make([]R, 0, len(src))
27+
for k, v := range src {
28+
res = append(res, project(k, v))
29+
}
30+
return res
31+
}
32+
33+
// ProjectMapToMap takes a map and a projection function and applies this function to each key-value pair in the map.
34+
// It returns a new map containing the results of the projection.
35+
// The original map is not modified.
36+
// Note that the resulting map may be smaller if the projection function does not guarantee unique keys.
37+
// If the projection function is nil, it returns nil.
38+
func ProjectMapToMap[K1 comparable, V1 any, K2 comparable, V2 any](src map[K1]V1, project func(K1, V1) (K2, V2)) map[K2]V2 {
39+
if project == nil {
40+
return nil
41+
}
42+
res := make(map[K2]V2, len(src))
43+
for k, v := range src {
44+
newK, newV := project(k, v)
45+
res[newK] = newV
46+
}
47+
return res
48+
}
49+
50+
// AggregateSlice takes a slice, an aggregation function and an initial value.
51+
// It applies the aggregation function to each element of the slice, also passing in the current result.
52+
// For the first element, it uses the initial value as the current result.
53+
// Returns initial if the aggregation function is nil.
54+
func AggregateSlice[X any, Y any](src []X, agg func(X, Y) Y, initial Y) Y {
55+
if agg == nil {
56+
return initial
57+
}
58+
res := initial
59+
for _, x := range src {
60+
res = agg(x, res)
61+
}
62+
return res
63+
}
64+
65+
// AggregateMap takes a map, an aggregation function and an initial value.
66+
// It applies the aggregation function to each key-value pair in the map, also passing in the current result.
67+
// For the first key-value pair, it uses the initial value as the current result.
68+
// Returns initial if the aggregation function is nil.
69+
func AggregateMap[K comparable, V any, R any](src map[K]V, agg func(K, V, R) R, initial R) R {
70+
if agg == nil {
71+
return initial
72+
}
73+
res := initial
74+
for k, v := range src {
75+
res = agg(k, v, res)
76+
}
77+
return res
78+
}

pkg/collections/utils_test.go

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package collections_test
2+
3+
import (
4+
. "github.com/onsi/ginkgo/v2"
5+
. "github.com/onsi/gomega"
6+
7+
"github.com/openmcp-project/controller-utils/pkg/collections"
8+
)
9+
10+
var _ = Describe("Utils Tests", func() {
11+
12+
Context("ProjectSlice", func() {
13+
14+
projectFunc := func(i int) int {
15+
return i * 2
16+
}
17+
18+
It("should use the projection function on each element of the slice", func() {
19+
src := []int{1, 2, 3, 4}
20+
projected := collections.ProjectSlice(src, projectFunc)
21+
Expect(projected).To(Equal([]int{2, 4, 6, 8}))
22+
Expect(src).To(Equal([]int{1, 2, 3, 4}), "original slice should not be modified")
23+
})
24+
25+
It("should return an empty slice for an empty or nil input slice", func() {
26+
Expect(collections.ProjectSlice(nil, projectFunc)).To(BeEmpty())
27+
Expect(collections.ProjectSlice([]int{}, projectFunc)).To(BeEmpty())
28+
})
29+
30+
It("should return nil for a nil projection function", func() {
31+
src := []int{1, 2, 3, 4}
32+
projected := collections.ProjectSlice[int, int](src, nil)
33+
Expect(projected).To(BeNil())
34+
Expect(src).To(Equal([]int{1, 2, 3, 4}), "original slice should not be modified")
35+
})
36+
37+
})
38+
39+
Context("ProjectMapToSlice", func() {
40+
41+
projectFunc := func(k string, v string) string {
42+
return k + ":" + v
43+
}
44+
45+
It("should use the projection function on each key-value pair of the map", func() {
46+
src := map[string]string{"a": "1", "b": "2", "c": "3"}
47+
projected := collections.ProjectMapToSlice(src, projectFunc)
48+
Expect(projected).To(ConsistOf("a:1", "b:2", "c:3"))
49+
Expect(src).To(Equal(map[string]string{"a": "1", "b": "2", "c": "3"}), "original map should not be modified")
50+
})
51+
52+
It("should return an empty slice for an empty or nil input map", func() {
53+
Expect(collections.ProjectMapToSlice(nil, projectFunc)).To(BeEmpty())
54+
Expect(collections.ProjectMapToSlice(map[string]string{}, projectFunc)).To(BeEmpty())
55+
})
56+
57+
It("should return nil for a nil projection function", func() {
58+
src := map[string]string{"a": "1", "b": "2", "c": "3"}
59+
projected := collections.ProjectMapToSlice[string, string, string](src, nil)
60+
Expect(projected).To(BeNil())
61+
Expect(src).To(Equal(map[string]string{"a": "1", "b": "2", "c": "3"}), "original map should not be modified")
62+
})
63+
64+
})
65+
66+
Context("ProjectMapToMap", func() {
67+
68+
projectFunc := func(k string, v string) (string, int) {
69+
return k, len(v)
70+
}
71+
72+
It("should use the projection function on each key-value pair of the map", func() {
73+
src := map[string]string{"a": "1", "b": "22", "c": "333"}
74+
projected := collections.ProjectMapToMap(src, projectFunc)
75+
Expect(projected).To(Equal(map[string]int{"a": 1, "b": 2, "c": 3}))
76+
Expect(src).To(Equal(map[string]string{"a": "1", "b": "22", "c": "333"}), "original map should not be modified")
77+
})
78+
79+
It("should return an empty map for an empty or nil input map", func() {
80+
Expect(collections.ProjectMapToMap(nil, projectFunc)).To(BeEmpty())
81+
Expect(collections.ProjectMapToMap(map[string]string{}, projectFunc)).To(BeEmpty())
82+
})
83+
84+
It("should return nil for a nil projection function", func() {
85+
src := map[string]string{"a": "1", "b": "22", "c": "333"}
86+
projected := collections.ProjectMapToMap[string, string, string, int](src, nil)
87+
Expect(projected).To(BeNil())
88+
Expect(src).To(Equal(map[string]string{"a": "1", "b": "22", "c": "333"}), "original map should not be modified")
89+
})
90+
91+
})
92+
93+
})

pkg/controller/utils.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,27 @@ func ObjectKey(name string, maybeNamespace ...string) client.ObjectKey {
5252
Name: name,
5353
}
5454
}
55+
56+
// RemoveFinalizerWithPrefix removes the first finalizer with the given prefix from the object.
57+
// The bool return value indicates whether a finalizer was removed.
58+
// If it is true, the string return value holds the suffix of the removed finalizer.
59+
// The logic is based on the controller-runtime's RemoveFinalizer function.
60+
func RemoveFinalizerWithPrefix(obj client.Object, prefix string) (string, bool) {
61+
fins := obj.GetFinalizers()
62+
length := len(fins)
63+
suffix := ""
64+
found := false
65+
66+
index := 0
67+
for i := range length {
68+
if !found && strings.HasPrefix(fins[i], prefix) {
69+
suffix = strings.TrimPrefix(fins[i], prefix)
70+
found = true
71+
continue
72+
}
73+
fins[index] = fins[i]
74+
index++
75+
}
76+
obj.SetFinalizers(fins[:index])
77+
return suffix, length != index
78+
}

0 commit comments

Comments
 (0)