From 21da7d56b450407b9968467971f3d9d5f92d0ca4 Mon Sep 17 00:00:00 2001 From: Johannes Aubart Date: Fri, 11 Jul 2025 14:29:55 +0200 Subject: [PATCH 1/5] add collection utility functions --- pkg/collections/maps/utils.go | 16 ++++- pkg/collections/maps/utils_test.go | 20 ++++++- pkg/collections/utils.go | 78 +++++++++++++++++++++++++ pkg/collections/utils_test.go | 93 ++++++++++++++++++++++++++++++ 4 files changed, 205 insertions(+), 2 deletions(-) create mode 100644 pkg/collections/utils.go create mode 100644 pkg/collections/utils_test.go diff --git a/pkg/collections/maps/utils.go b/pkg/collections/maps/utils.go index 38f33e5..77a5153 100644 --- a/pkg/collections/maps/utils.go +++ b/pkg/collections/maps/utils.go @@ -1,6 +1,11 @@ package maps -import "github.com/openmcp-project/controller-utils/pkg/collections/filters" +import ( + "k8s.io/utils/ptr" + + "github.com/openmcp-project/controller-utils/pkg/collections/filters" + "github.com/openmcp-project/controller-utils/pkg/pairs" +) // Filter filters a map by applying a filter function to each key-value pair. // Only the entries for which the filter function returns true are kept in the copy. @@ -50,3 +55,12 @@ func Intersect[K comparable, V any](source map[K]V, maps ...map[K]V) map[K]V { return res } + +// GetAny returns an arbitrary key-value pair from the map as a pointer to a pairs.Pair. +// If the map is empty, it returns nil. +func GetAny[K comparable, V any](m map[K]V) *pairs.Pair[K, V] { + for k, v := range m { + return ptr.To(pairs.New(k, v)) + } + return nil +} diff --git a/pkg/collections/maps/utils_test.go b/pkg/collections/maps/utils_test.go index e3f456b..f27efbb 100644 --- a/pkg/collections/maps/utils_test.go +++ b/pkg/collections/maps/utils_test.go @@ -7,7 +7,7 @@ import ( "github.com/openmcp-project/controller-utils/pkg/collections/maps" ) -var _ = Describe("LinkedIterator Tests", func() { +var _ = Describe("Map Utils Tests", func() { Context("Merge", func() { @@ -60,4 +60,22 @@ var _ = Describe("LinkedIterator Tests", func() { }) + Context("GetAny", func() { + + It("should return a key-value pair from the map", func() { + m1 := map[string]string{"foo": "bar", "bar": "baz", "foobar": "foobaz"} + pair := maps.GetAny(m1) + Expect(pair).ToNot(BeNil()) + Expect(pair.Key).To(BeElementOf("foo", "bar", "foobar")) + Expect(m1[pair.Key]).To(Equal(pair.Value)) + }) + + It("should return nil for an empty or nil map", func() { + var nilMap map[string]string + Expect(maps.GetAny(nilMap)).To(BeNil()) + Expect(maps.GetAny(map[string]string{})).To(BeNil()) + }) + + }) + }) diff --git a/pkg/collections/utils.go b/pkg/collections/utils.go new file mode 100644 index 0000000..f5b03f2 --- /dev/null +++ b/pkg/collections/utils.go @@ -0,0 +1,78 @@ +package collections + +// ProjectSlice takes a slice and a projection function and applies this function to each element of the slice. +// It returns a new slice containing the results of the projection. +// The original slice is not modified. +// If the projection function is nil, it returns nil. +func ProjectSlice[X any, Y any](src []X, project func(X) Y) []Y { + if project == nil { + return nil + } + res := make([]Y, len(src)) + for i, src := range src { + res[i] = project(src) + } + return res +} + +// ProjectMapToSlice takes a map and a projection function and applies this function to each key-value pair in the map. +// It returns a new slice containing the results of the projection. +// The original map is not modified. +// If the projection function is nil, it returns nil. +func ProjectMapToSlice[K comparable, V any, R any](src map[K]V, project func(K, V) R) []R { + if project == nil { + return nil + } + res := make([]R, 0, len(src)) + for k, v := range src { + res = append(res, project(k, v)) + } + return res +} + +// ProjectMapToMap takes a map and a projection function and applies this function to each key-value pair in the map. +// It returns a new map containing the results of the projection. +// The original map is not modified. +// Note that the resulting map may be smaller if the projection function does not guarantee unique keys. +// If the projection function is nil, it returns nil. +func ProjectMapToMap[K1 comparable, V1 any, K2 comparable, V2 any](src map[K1]V1, project func(K1, V1) (K2, V2)) map[K2]V2 { + if project == nil { + return nil + } + res := make(map[K2]V2, len(src)) + for k, v := range src { + newK, newV := project(k, v) + res[newK] = newV + } + return res +} + +// AggregateSlice takes a slice, an aggregation function and an initial value. +// It applies the aggregation function to each element of the slice, also passing in the current result. +// For the first element, it uses the initial value as the current result. +// Returns initial if the aggregation function is nil. +func AggregateSlice[X any, Y any](src []X, agg func(X, Y) Y, initial Y) Y { + if agg == nil { + return initial + } + res := initial + for _, x := range src { + res = agg(x, res) + } + return res +} + +// AggregateMap takes a map, an aggregation function and an initial value. +// It applies the aggregation function to each key-value pair in the map, also passing in the current result. +// For the first key-value pair, it uses the initial value as the current result. +// Returns initial if the aggregation function is nil. +func AggregateMap[K comparable, V any, R any](src map[K]V, agg func(K, V, R) R, initial R) R { + if agg == nil { + return initial + } + res := initial + for k, v := range src { + res = agg(k, v, res) + } + return res +} diff --git a/pkg/collections/utils_test.go b/pkg/collections/utils_test.go new file mode 100644 index 0000000..1948fe9 --- /dev/null +++ b/pkg/collections/utils_test.go @@ -0,0 +1,93 @@ +package collections_test + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/openmcp-project/controller-utils/pkg/collections" +) + +var _ = Describe("Utils Tests", func() { + + Context("ProjectSlice", func() { + + projectFunc := func(i int) int { + return i * 2 + } + + It("should use the projection function on each element of the slice", func() { + src := []int{1, 2, 3, 4} + projected := collections.ProjectSlice(src, projectFunc) + Expect(projected).To(Equal([]int{2, 4, 6, 8})) + Expect(src).To(Equal([]int{1, 2, 3, 4}), "original slice should not be modified") + }) + + It("should return an empty slice for an empty or nil input slice", func() { + Expect(collections.ProjectSlice(nil, projectFunc)).To(BeEmpty()) + Expect(collections.ProjectSlice([]int{}, projectFunc)).To(BeEmpty()) + }) + + It("should return nil for a nil projection function", func() { + src := []int{1, 2, 3, 4} + projected := collections.ProjectSlice[int, int](src, nil) + Expect(projected).To(BeNil()) + Expect(src).To(Equal([]int{1, 2, 3, 4}), "original slice should not be modified") + }) + + }) + + Context("ProjectMapToSlice", func() { + + projectFunc := func(k string, v string) string { + return k + ":" + v + } + + It("should use the projection function on each key-value pair of the map", func() { + src := map[string]string{"a": "1", "b": "2", "c": "3"} + projected := collections.ProjectMapToSlice(src, projectFunc) + Expect(projected).To(ConsistOf("a:1", "b:2", "c:3")) + Expect(src).To(Equal(map[string]string{"a": "1", "b": "2", "c": "3"}), "original map should not be modified") + }) + + It("should return an empty slice for an empty or nil input map", func() { + Expect(collections.ProjectMapToSlice(nil, projectFunc)).To(BeEmpty()) + Expect(collections.ProjectMapToSlice(map[string]string{}, projectFunc)).To(BeEmpty()) + }) + + It("should return nil for a nil projection function", func() { + src := map[string]string{"a": "1", "b": "2", "c": "3"} + projected := collections.ProjectMapToSlice[string, string, string](src, nil) + Expect(projected).To(BeNil()) + Expect(src).To(Equal(map[string]string{"a": "1", "b": "2", "c": "3"}), "original map should not be modified") + }) + + }) + + Context("ProjectMapToMap", func() { + + projectFunc := func(k string, v string) (string, int) { + return k, len(v) + } + + It("should use the projection function on each key-value pair of the map", func() { + src := map[string]string{"a": "1", "b": "22", "c": "333"} + projected := collections.ProjectMapToMap(src, projectFunc) + Expect(projected).To(Equal(map[string]int{"a": 1, "b": 2, "c": 3})) + Expect(src).To(Equal(map[string]string{"a": "1", "b": "22", "c": "333"}), "original map should not be modified") + }) + + It("should return an empty map for an empty or nil input map", func() { + Expect(collections.ProjectMapToMap(nil, projectFunc)).To(BeEmpty()) + Expect(collections.ProjectMapToMap(map[string]string{}, projectFunc)).To(BeEmpty()) + }) + + It("should return nil for a nil projection function", func() { + src := map[string]string{"a": "1", "b": "22", "c": "333"} + projected := collections.ProjectMapToMap[string, string, string, int](src, nil) + Expect(projected).To(BeNil()) + Expect(src).To(Equal(map[string]string{"a": "1", "b": "22", "c": "333"}), "original map should not be modified") + }) + + }) + +}) From 4c455ed75999441b20324f26d68e047ff253ed51 Mon Sep 17 00:00:00 2001 From: Johannes Aubart Date: Fri, 11 Jul 2025 14:30:41 +0200 Subject: [PATCH 2/5] add RemoveFinalizersWithPrefix method and rewrite utils tests --- pkg/controller/utils.go | 26 ++++++++ pkg/controller/utils_test.go | 115 ++++++++++++++++++++++++----------- 2 files changed, 105 insertions(+), 36 deletions(-) diff --git a/pkg/controller/utils.go b/pkg/controller/utils.go index 44423a5..b78baa1 100644 --- a/pkg/controller/utils.go +++ b/pkg/controller/utils.go @@ -52,3 +52,29 @@ func ObjectKey(name string, maybeNamespace ...string) client.ObjectKey { Name: name, } } + +// RemoveFinalizersWithPrefix removes finalizers with a given prefix from the object and returns their suffixes. +// If the third argument is true, all finalizers with the given prefix are removed, otherwise only the first one. +// The bool return value indicates whether a finalizer was removed. +// If it is true, the slice return value holds the suffixes of all removed finalizers (will be of length 1 if removeAll is false). +// If it is false, no finalizer with the given prefix was found. The slice return value will be empty in this case. +// The logic is based on the controller-runtime's RemoveFinalizer function. +func RemoveFinalizersWithPrefix(obj client.Object, prefix string, removeAll bool) ([]string, bool) { + fins := obj.GetFinalizers() + length := len(fins) + suffixes := make([]string, 0, length) + found := false + + index := 0 + for i := range length { + if (removeAll || !found) && strings.HasPrefix(fins[i], prefix) { + suffixes = append(suffixes, strings.TrimPrefix(fins[i], prefix)) + found = true + continue + } + fins[index] = fins[i] + index++ + } + obj.SetFinalizers(fins[:index]) + return suffixes, length != index +} diff --git a/pkg/controller/utils_test.go b/pkg/controller/utils_test.go index 431615b..22f015c 100644 --- a/pkg/controller/utils_test.go +++ b/pkg/controller/utils_test.go @@ -2,49 +2,92 @@ package controller import ( "fmt" - "testing" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/openmcp-project/controller-utils/pkg/pairs" + + corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/util/validation" ) -func TestK8sNameHash(t *testing.T) { - tt := []struct { - input []string - expHash string - }{ - { - []string{"test1"}, - "dnhq5gcrs4mzrzzsa6cujsllg3b5ahhn67fkgmrvtvxr3a2woaka", - }, - { - // check that the same string produces the same hash - []string{"test1"}, - "dnhq5gcrs4mzrzzsa6cujsllg3b5ahhn67fkgmrvtvxr3a2woaka", - }, - { - []string{"bla"}, - "jxz4h5upzsb3e7u5ileqimnhesm7c6dvzanftg2wnsmitoljm4bq", - }, - { - []string{"some other test", "this is a very, very long string"}, - "rjphpfjbmwn6qqydv6xhtmj3kxrlzepn2tpwy4okw2ypoc3nlffq", - }, - } - - for _, tc := range tt { - t.Run(fmt.Sprint(tc.input), func(t *testing.T) { - res := K8sNameHash(tc.input...) - - if res != tc.expHash { - t.Errorf("exp hash %q, got %q", tc.expHash, res) +var _ = Describe("Predicates", func() { + + Context("K8sNameHash", func() { + + testData := []pairs.Pair[*[]string, string]{ + { + Key: &[]string{"test1"}, + Value: "dnhq5gcrs4mzrzzsa6cujsllg3b5ahhn67fkgmrvtvxr3a2woaka", + }, + { + Key: &[]string{"bla"}, + Value: "jxz4h5upzsb3e7u5ileqimnhesm7c6dvzanftg2wnsmitoljm4bq", + }, + { + Key: &[]string{"some other test", "this is a very, very long string"}, + Value: "rjphpfjbmwn6qqydv6xhtmj3kxrlzepn2tpwy4okw2ypoc3nlffq", + }, + } + + It("should generate the same hash for the same input value", func() { + for _, p := range testData { + for range 5 { + res := K8sNameHash(*p.Key...) + Expect(res).To(Equal(p.Value)) + } } + }) + + It("should generate different hashes for different input values", func() { + res1 := K8sNameHash(*testData[0].Key...) + res2 := K8sNameHash(*testData[1].Key...) + res3 := K8sNameHash(*testData[2].Key...) + Expect(res1).NotTo(Equal(res2)) + Expect(res1).NotTo(Equal(res3)) + Expect(res2).NotTo(Equal(res3)) + }) - // ensure the result is a valid DNS1123Subdomain - if errs := validation.IsDNS1123Subdomain(res); errs != nil { - t.Errorf("value %q is invalid: %v", res, errs) + It("should generate a valid DNS1123Subdomain", func() { + for _, p := range testData { + res := K8sNameHash(*p.Key...) + errs := validation.IsDNS1123Subdomain(res) + Expect(errs).To(BeEmpty(), fmt.Sprintf("value %q is invalid: %v", res, errs)) } + }) + + }) + + Context("RemoveFinalizersWithPrefix", func() { + + It("should only remove the first finalizer with the given prefix", func() { + ns := &corev1.Namespace{} + ns.SetFinalizers([]string{"foo/bar", "baz/qux", "foo/baz"}) + suffix, removed := RemoveFinalizersWithPrefix(ns, "foo/", false) + Expect(removed).To(BeTrue()) + Expect(suffix).To(ConsistOf("bar")) + Expect(ns.GetFinalizers()).To(Equal([]string{"baz/qux", "foo/baz"}), "should remove only the first matching finalizer") + }) + + It("should remove all finalizers with the given prefix", func() { + ns := &corev1.Namespace{} + ns.SetFinalizers([]string{"foo/bar", "baz/qux", "foo/baz"}) + suffix, removed := RemoveFinalizersWithPrefix(ns, "foo/", true) + Expect(removed).To(BeTrue()) + Expect(suffix).To(ConsistOf("bar", "baz")) + Expect(ns.GetFinalizers()).To(Equal([]string{"baz/qux"}), "should remove all matching finalizers") + }) + It("should return false if no finalizer with the given prefix exists", func() { + ns := &corev1.Namespace{} + ns.SetFinalizers([]string{"foo/bar", "baz/qux"}) + suffix, removed := RemoveFinalizersWithPrefix(ns, "nonexistent/", false) + Expect(removed).To(BeFalse()) + Expect(suffix).To(BeEmpty()) + Expect(ns.GetFinalizers()).To(Equal([]string{"foo/bar", "baz/qux"}), "should not modify finalizers if no match is found") }) - } -} + }) + +}) From 7a80bbe6402b49efbc8adc76170f1625ee572cc9 Mon Sep 17 00:00:00 2001 From: Johannes Aubart Date: Mon, 14 Jul 2025 13:58:25 +0200 Subject: [PATCH 3/5] add unit tests for AggregateSlice and AggregateMap --- pkg/collections/utils.go | 1 + pkg/collections/utils_test.go | 69 +++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+) diff --git a/pkg/collections/utils.go b/pkg/collections/utils.go index f5b03f2..068bac3 100644 --- a/pkg/collections/utils.go +++ b/pkg/collections/utils.go @@ -66,6 +66,7 @@ func AggregateSlice[X any, Y any](src []X, agg func(X, Y) Y, initial Y) Y { // It applies the aggregation function to each key-value pair in the map, also passing in the current result. // For the first key-value pair, it uses the initial value as the current result. // Returns initial if the aggregation function is nil. +// Note that the iteration order over the map elements is undefined and may vary between executions. func AggregateMap[K comparable, V any, R any](src map[K]V, agg func(K, V, R) R, initial R) R { if agg == nil { return initial diff --git a/pkg/collections/utils_test.go b/pkg/collections/utils_test.go index 1948fe9..9d2e61d 100644 --- a/pkg/collections/utils_test.go +++ b/pkg/collections/utils_test.go @@ -1,10 +1,13 @@ package collections_test import ( + "fmt" + . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "github.com/openmcp-project/controller-utils/pkg/collections" + "github.com/openmcp-project/controller-utils/pkg/pairs" ) var _ = Describe("Utils Tests", func() { @@ -90,4 +93,70 @@ var _ = Describe("Utils Tests", func() { }) + Context("AggregateSlice", func() { + + sum := func(val, s int) int { + return val + s + } + stradd := func(val int, s string) string { + return fmt.Sprintf("%s%d", s, val) + } + + It("should return the initial value if the aggregation function is nil", func() { + src := []int{1, 2, 3, 4} + result := collections.AggregateSlice(src, nil, 0) + Expect(result).To(Equal(0)) + Expect(src).To(Equal([]int{1, 2, 3, 4})) + }) + + It("should correctly aggregate the slice using the provided function", func() { + src := []int{1, 2, 3, 4} + result := collections.AggregateSlice(src, sum, 0) + Expect(result).To(Equal(10)) + Expect(src).To(Equal([]int{1, 2, 3, 4})) + + result2 := collections.AggregateSlice(src, stradd, "test") + Expect(result2).To(Equal("test1234")) + Expect(src).To(Equal([]int{1, 2, 3, 4})) + }) + + It("should handle a nil input slice", func() { + result := collections.AggregateSlice[int, int](nil, sum, 100) + Expect(result).To(Equal(100)) + }) + + }) + + Context("AggregateMap", func() { + + aggregate := func(k string, v int, agg pairs.Pair[string, int]) pairs.Pair[string, int] { + return pairs.New(agg.Key+k, agg.Value+v) + } + + It("should return the initial value if the aggregation function is nil", func() { + src := map[string]int{"a": 1, "b": 2, "c": 3} + result := collections.AggregateMap(src, nil, 0) + Expect(result).To(Equal(0)) + Expect(src).To(Equal(map[string]int{"a": 1, "b": 2, "c": 3})) + }) + + It("should correctly aggregate the map using the provided function", func() { + src := map[string]int{"a": 1, "b": 2, "c": 3} + result := collections.AggregateMap(src, aggregate, pairs.New("", 0)) + Expect(result.Key).To(HaveLen(3)) + Expect(result.Key).To(ContainSubstring("a")) + Expect(result.Key).To(ContainSubstring("b")) + Expect(result.Key).To(ContainSubstring("c")) + Expect(result.Value).To(Equal(6)) + Expect(src).To(Equal(map[string]int{"a": 1, "b": 2, "c": 3})) + }) + + It("should handle a nil input map", func() { + result := collections.AggregateMap(nil, aggregate, pairs.New("", 0)) + Expect(result.Key).To(BeEmpty()) + Expect(result.Value).To(Equal(0)) + }) + + }) + }) From 2a4ff14b3d134d4d7da4dc69a75ec71ff5c08faa Mon Sep 17 00:00:00 2001 From: Johannes Aubart Date: Mon, 14 Jul 2025 15:09:26 +0200 Subject: [PATCH 4/5] remove RemoveFinalizersWithPrefix function because it is somewhat specific and not used currently --- pkg/controller/utils.go | 26 -------------------------- pkg/controller/utils_test.go | 32 -------------------------------- 2 files changed, 58 deletions(-) diff --git a/pkg/controller/utils.go b/pkg/controller/utils.go index b78baa1..44423a5 100644 --- a/pkg/controller/utils.go +++ b/pkg/controller/utils.go @@ -52,29 +52,3 @@ func ObjectKey(name string, maybeNamespace ...string) client.ObjectKey { Name: name, } } - -// RemoveFinalizersWithPrefix removes finalizers with a given prefix from the object and returns their suffixes. -// If the third argument is true, all finalizers with the given prefix are removed, otherwise only the first one. -// The bool return value indicates whether a finalizer was removed. -// If it is true, the slice return value holds the suffixes of all removed finalizers (will be of length 1 if removeAll is false). -// If it is false, no finalizer with the given prefix was found. The slice return value will be empty in this case. -// The logic is based on the controller-runtime's RemoveFinalizer function. -func RemoveFinalizersWithPrefix(obj client.Object, prefix string, removeAll bool) ([]string, bool) { - fins := obj.GetFinalizers() - length := len(fins) - suffixes := make([]string, 0, length) - found := false - - index := 0 - for i := range length { - if (removeAll || !found) && strings.HasPrefix(fins[i], prefix) { - suffixes = append(suffixes, strings.TrimPrefix(fins[i], prefix)) - found = true - continue - } - fins[index] = fins[i] - index++ - } - obj.SetFinalizers(fins[:index]) - return suffixes, length != index -} diff --git a/pkg/controller/utils_test.go b/pkg/controller/utils_test.go index 22f015c..e690f6c 100644 --- a/pkg/controller/utils_test.go +++ b/pkg/controller/utils_test.go @@ -8,7 +8,6 @@ import ( "github.com/openmcp-project/controller-utils/pkg/pairs" - corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/util/validation" ) @@ -59,35 +58,4 @@ var _ = Describe("Predicates", func() { }) - Context("RemoveFinalizersWithPrefix", func() { - - It("should only remove the first finalizer with the given prefix", func() { - ns := &corev1.Namespace{} - ns.SetFinalizers([]string{"foo/bar", "baz/qux", "foo/baz"}) - suffix, removed := RemoveFinalizersWithPrefix(ns, "foo/", false) - Expect(removed).To(BeTrue()) - Expect(suffix).To(ConsistOf("bar")) - Expect(ns.GetFinalizers()).To(Equal([]string{"baz/qux", "foo/baz"}), "should remove only the first matching finalizer") - }) - - It("should remove all finalizers with the given prefix", func() { - ns := &corev1.Namespace{} - ns.SetFinalizers([]string{"foo/bar", "baz/qux", "foo/baz"}) - suffix, removed := RemoveFinalizersWithPrefix(ns, "foo/", true) - Expect(removed).To(BeTrue()) - Expect(suffix).To(ConsistOf("bar", "baz")) - Expect(ns.GetFinalizers()).To(Equal([]string{"baz/qux"}), "should remove all matching finalizers") - }) - - It("should return false if no finalizer with the given prefix exists", func() { - ns := &corev1.Namespace{} - ns.SetFinalizers([]string{"foo/bar", "baz/qux"}) - suffix, removed := RemoveFinalizersWithPrefix(ns, "nonexistent/", false) - Expect(removed).To(BeFalse()) - Expect(suffix).To(BeEmpty()) - Expect(ns.GetFinalizers()).To(Equal([]string{"foo/bar", "baz/qux"}), "should not modify finalizers if no match is found") - }) - - }) - }) From c5b866a80983a7a985014b973f0436a846538b0c Mon Sep 17 00:00:00 2001 From: Johannes Aubart Date: Mon, 14 Jul 2025 16:19:54 +0200 Subject: [PATCH 5/5] rename variable to avoid confusing overshadowing --- pkg/collections/utils.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/collections/utils.go b/pkg/collections/utils.go index 068bac3..3c2c1dc 100644 --- a/pkg/collections/utils.go +++ b/pkg/collections/utils.go @@ -9,8 +9,8 @@ func ProjectSlice[X any, Y any](src []X, project func(X) Y) []Y { return nil } res := make([]Y, len(src)) - for i, src := range src { - res[i] = project(src) + for i, x := range src { + res[i] = project(x) } return res }