diff --git a/configmap/parse.go b/configmap/parse.go index d8caba3c3f..bea5b5ea4d 100644 --- a/configmap/parse.go +++ b/configmap/parse.go @@ -18,7 +18,6 @@ package configmap import ( "fmt" - "strconv" "strings" "time" @@ -26,144 +25,44 @@ import ( "k8s.io/apimachinery/pkg/api/validation" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/sets" + "knative.dev/pkg/configmap/parser" ) // ParseFunc is a function taking ConfigMap data and applying a parse operation to it. -type ParseFunc func(map[string]string) error +type ParseFunc = parser.ParseFunc // AsString passes the value at key through into the target, if it exists. -func AsString(key string, target *string) ParseFunc { - return func(data map[string]string) error { - if raw, ok := data[key]; ok { - *target = raw - } - return nil - } -} +var AsString = parser.As[string] // AsBool parses the value at key as a boolean into the target, if it exists. -func AsBool(key string, target *bool) ParseFunc { - return func(data map[string]string) error { - if raw, ok := data[key]; ok { - val, err := strconv.ParseBool(raw) - *target = val // If err != nil — this is always false. - return err - } - return nil - } -} +var AsBool = parser.As[bool] // AsInt16 parses the value at key as an int16 into the target, if it exists. -func AsInt16(key string, target *int16) ParseFunc { - return func(data map[string]string) error { - if raw, ok := data[key]; ok { - val, err := strconv.ParseInt(raw, 10, 16) - if err != nil { - return fmt.Errorf("failed to parse %q: %w", key, err) - } - *target = int16(val) - } - return nil - } -} +var AsInt16 = parser.As[int16] // AsInt32 parses the value at key as an int32 into the target, if it exists. -func AsInt32(key string, target *int32) ParseFunc { - return func(data map[string]string) error { - if raw, ok := data[key]; ok { - val, err := strconv.ParseInt(raw, 10, 32) - if err != nil { - return fmt.Errorf("failed to parse %q: %w", key, err) - } - *target = int32(val) - } - return nil - } -} +var AsInt32 = parser.As[int32] // AsInt64 parses the value at key as an int64 into the target, if it exists. -func AsInt64(key string, target *int64) ParseFunc { - return func(data map[string]string) error { - if raw, ok := data[key]; ok { - val, err := strconv.ParseInt(raw, 10, 64) - if err != nil { - return fmt.Errorf("failed to parse %q: %w", key, err) - } - *target = val - } - return nil - } -} +var AsInt64 = parser.As[int64] // AsInt parses the value at key as an int into the target, if it exists. -func AsInt(key string, target *int) ParseFunc { - return func(data map[string]string) error { - if raw, ok := data[key]; ok { - val, err := strconv.Atoi(raw) - if err != nil { - return fmt.Errorf("failed to parse %q: %w", key, err) - } - *target = val - } - return nil - } -} +var AsInt = parser.As[int] // AsUint16 parses the value at key as an uint16 into the target, if it exists. -func AsUint16(key string, target *uint16) ParseFunc { - return func(data map[string]string) error { - if raw, ok := data[key]; ok { - val, err := strconv.ParseUint(raw, 10, 16) - if err != nil { - return fmt.Errorf("failed to parse %q: %w", key, err) - } - *target = uint16(val) - } - return nil - } -} +var AsUint16 = parser.As[uint16] // AsUint32 parses the value at key as an uint32 into the target, if it exists. -func AsUint32(key string, target *uint32) ParseFunc { - return func(data map[string]string) error { - if raw, ok := data[key]; ok { - val, err := strconv.ParseUint(raw, 10, 32) - if err != nil { - return fmt.Errorf("failed to parse %q: %w", key, err) - } - *target = uint32(val) - } - return nil - } -} +var AsUint32 = parser.As[uint32] + +// AsUint64 parses the value at key as an uint32 into the target, if it exists. +var AsUint64 = parser.As[uint32] // AsFloat64 parses the value at key as a float64 into the target, if it exists. -func AsFloat64(key string, target *float64) ParseFunc { - return func(data map[string]string) error { - if raw, ok := data[key]; ok { - val, err := strconv.ParseFloat(raw, 64) - if err != nil { - return fmt.Errorf("failed to parse %q: %w", key, err) - } - *target = val - } - return nil - } -} +var AsFloat64 = parser.As[float64] // AsDuration parses the value at key as a time.Duration into the target, if it exists. -func AsDuration(key string, target *time.Duration) ParseFunc { - return func(data map[string]string) error { - if raw, ok := data[key]; ok { - val, err := time.ParseDuration(raw) - if err != nil { - return fmt.Errorf("failed to parse %q: %w", key, err) - } - *target = val - } - return nil - } -} +var AsDuration = parser.As[time.Duration] // AsStringSet parses the value at key as a sets.Set[string] (split by ',') into the target, if it exists. func AsStringSet(key string, target *sets.Set[string]) ParseFunc { diff --git a/configmap/parse_test.go b/configmap/parse_test.go index 9807263e71..426769d967 100644 --- a/configmap/parse_test.go +++ b/configmap/parse_test.go @@ -18,7 +18,6 @@ package configmap import ( "testing" - "time" "github.com/google/go-cmp/cmp" "k8s.io/apimachinery/pkg/api/resource" @@ -27,18 +26,8 @@ import ( ) type testConfig struct { - str string - toggle bool - i16 int16 - i32 int32 - i64 int64 - u16 uint16 - u32 uint32 - i int - f64 float64 - dur time.Duration - set sets.Set[string] - qua *resource.Quantity + set sets.Set[string] + qua *resource.Quantity nsn types.NamespacedName onsn *types.NamespacedName @@ -57,16 +46,6 @@ func TestParse(t *testing.T) { }{{ name: "all good", data: map[string]string{ - "test-string": "foo.bar", - "test-bool": "true", - "test-int16": "6", - "test-int32": "1", - "test-int64": "2", - "test-uint16": "5", - "test-uint32": "3", - "test-int": "4", - "test-float64": "1.0", - "test-duration": "1m", "test-set": "a,b,c, d", "test-quantity": "500m", @@ -77,18 +56,8 @@ func TestParse(t *testing.T) { "test-dict.k1": "v1", }, want: testConfig{ - str: "foo.bar", - toggle: true, - i16: 6, - i32: 1, - i64: 2, - u16: 5, - u32: 3, - f64: 1.0, - i: 4, - dur: time.Minute, - set: sets.New[string]("a", "b", "c", "d"), - qua: &fiveHundredM, + set: sets.New("a", "b", "c", "d"), + qua: &fiveHundredM, nsn: types.NamespacedName{ Name: "some-name", Namespace: "some-namespace", @@ -105,67 +74,11 @@ func TestParse(t *testing.T) { }, { name: "respect defaults", conf: testConfig{ - str: "foo.bar", - toggle: true, - i32: 1, - i64: 2, - f64: 1.0, - i: 4, - dur: time.Minute, - qua: &fiveHundredM, + qua: &fiveHundredM, }, want: testConfig{ - str: "foo.bar", - toggle: true, - i32: 1, - i64: 2, - f64: 1.0, - i: 4, - dur: time.Minute, - qua: &fiveHundredM, + qua: &fiveHundredM, }, - }, { - name: "junk bool fails", - data: map[string]string{ - "test-bool": "foo", - }, - expectErr: true, - }, { - name: "int32 error", - data: map[string]string{ - "test-int32": "foo", - }, - expectErr: true, - }, { - name: "int64 error", - data: map[string]string{ - "test-int64": "foo", - }, - expectErr: true, - }, { - name: "uint32 error", - data: map[string]string{ - "test-uint32": "foo", - }, - expectErr: true, - }, { - name: "int error", - data: map[string]string{ - "test-int": "foo", - }, - expectErr: true, - }, { - name: "float64 error", - data: map[string]string{ - "test-float64": "foo", - }, - expectErr: true, - }, { - name: "duration error", - data: map[string]string{ - "test-duration": "foo", - }, - expectErr: true, }, { name: "quantity error", data: map[string]string{ @@ -201,16 +114,6 @@ func TestParse(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { if err := Parse(test.data, - AsString("test-string", &test.conf.str), - AsBool("test-bool", &test.conf.toggle), - AsInt16("test-int16", &test.conf.i16), - AsInt32("test-int32", &test.conf.i32), - AsInt64("test-int64", &test.conf.i64), - AsUint16("test-uint16", &test.conf.u16), - AsUint32("test-uint32", &test.conf.u32), - AsInt("test-int", &test.conf.i), - AsFloat64("test-float64", &test.conf.f64), - AsDuration("test-duration", &test.conf.dur), AsStringSet("test-set", &test.conf.set), AsQuantity("test-quantity", &test.conf.qua), AsNamespacedName("test-namespaced-name", &test.conf.nsn), diff --git a/configmap/parser/parse.go b/configmap/parser/parse.go new file mode 100644 index 0000000000..a468727aa9 --- /dev/null +++ b/configmap/parser/parse.go @@ -0,0 +1,109 @@ +/* +Copyright 2025 The Knative Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package parser + +import ( + "fmt" + "strconv" + "time" +) + +// ParseFunc is a function taking ConfigMap data and applying a parse operation to it. +type ParseFunc func(map[string]string) error + +func Parse(data map[string]string, parsers ...ParseFunc) error { + for _, parse := range parsers { + if err := parse(data); err != nil { + return err + } + } + return nil +} + +func AsFunc[T any]( + key string, + target *T, + parseVal func(s string) (T, error), +) ParseFunc { + return func(data map[string]string) error { + if raw, ok := data[key]; ok { + val, err := parseVal(raw) + if err != nil { + return fmt.Errorf("failed to parse %q: %w", key, err) + } + *target = val + } + return nil + } +} + +func As[T parseable](key string, target *T) ParseFunc { + return AsFunc(key, target, parse) +} + +type parseable interface { + int | int16 | int32 | int64 | + uint | uint16 | uint32 | uint64 | + string | bool | float64 | float32 | + time.Duration +} + +//nolint:gosec // ignore integer overflow +func parse[T parseable](s string) (T, error) { + var zero T + + var val any + var err error + + switch any(zero).(type) { + case string: + val = s + case int16: + val, err = strconv.ParseInt(s, 10, 16) + val = int16(val.(int64)) + case int32: + val, err = strconv.ParseInt(s, 10, 32) + val = int32(val.(int64)) + case int64: + val, err = strconv.ParseInt(s, 10, 64) + case uint16: + val, err = strconv.ParseUint(s, 10, 16) + val = uint16(val.(uint64)) + case uint32: + val, err = strconv.ParseUint(s, 10, 32) + val = uint32(val.(uint64)) + case uint64: + val, err = strconv.ParseUint(s, 10, 64) + case float64: + val, err = strconv.ParseFloat(s, 64) + case float32: + val, err = strconv.ParseFloat(s, 64) + val = float32(val.(float64)) + case bool: + val, err = strconv.ParseBool(s) + case time.Duration: + val, err = time.ParseDuration(s) + case int: + val, err = strconv.ParseInt(s, 10, 0) + val = int(val.(int64)) + case uint: + val, err = strconv.ParseUint(s, 10, 0) + val = uint(val.(uint64)) + } + + return val.(T), err +} diff --git a/configmap/parser/parse_test.go b/configmap/parser/parse_test.go new file mode 100644 index 0000000000..2135dca54f --- /dev/null +++ b/configmap/parser/parse_test.go @@ -0,0 +1,178 @@ +/* +Copyright 2025 The Knative Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package parser + +import ( + "testing" + "time" + + "github.com/google/go-cmp/cmp" +) + +type testConfig struct { + str string + toggle bool + i16 int16 + i32 int32 + i64 int64 + u16 uint16 + u32 uint32 + u64 uint64 + i int + ui uint + f32 float32 + f64 float64 + dur time.Duration +} + +func TestParse(t *testing.T) { + tests := []struct { + name string + conf testConfig + data map[string]string + want testConfig + expectErr bool + }{{ + name: "all good", + data: map[string]string{ + "test-string": "foo.bar", + "test-bool": "true", + "test-int16": "6", + "test-int32": "1", + "test-int64": "2", + "test-uint16": "5", + "test-uint32": "3", + "test-uint64": "6", + "test-int": "4", + "test-uint": "5", + "test-float32": "1.2", + "test-float64": "1.0", + "test-duration": "1m", + "test-set": "a,b,c, d", + "test-quantity": "500m", + + "test-namespaced-name": "some-namespace/some-name", + "test-optional-namespaced-name": "some-other-namespace/some-other-name", + + "test-dict.k": "v", + "test-dict.k1": "v1", + }, + want: testConfig{ + str: "foo.bar", + toggle: true, + i16: 6, + i32: 1, + i64: 2, + u16: 5, + u32: 3, + u64: 6, + f32: 1.2, + f64: 1.0, + i: 4, + ui: 5, + dur: time.Minute, + }, + }, { + name: "respect defaults", + conf: testConfig{ + str: "foo.bar", + toggle: true, + i32: 1, + i64: 2, + f64: 1.0, + i: 4, + dur: time.Minute, + }, + want: testConfig{ + str: "foo.bar", + toggle: true, + i32: 1, + i64: 2, + f64: 1.0, + i: 4, + dur: time.Minute, + }, + }, { + name: "junk bool fails", + data: map[string]string{ + "test-bool": "foo", + }, + expectErr: true, + }, { + name: "int32 error", + data: map[string]string{ + "test-int32": "foo", + }, + expectErr: true, + }, { + name: "int64 error", + data: map[string]string{ + "test-int64": "foo", + }, + expectErr: true, + }, { + name: "uint32 error", + data: map[string]string{ + "test-uint32": "foo", + }, + expectErr: true, + }, { + name: "int error", + data: map[string]string{ + "test-int": "foo", + }, + expectErr: true, + }, { + name: "float64 error", + data: map[string]string{ + "test-float64": "foo", + }, + expectErr: true, + }, { + name: "duration error", + data: map[string]string{ + "test-duration": "foo", + }, + expectErr: true, + }} + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if err := Parse(test.data, + As("test-string", &test.conf.str), + As("test-bool", &test.conf.toggle), + As("test-int16", &test.conf.i16), + As("test-int32", &test.conf.i32), + As("test-int64", &test.conf.i64), + As("test-uint16", &test.conf.u16), + As("test-uint32", &test.conf.u32), + As("test-uint64", &test.conf.u64), + As("test-int", &test.conf.i), + As("test-uint", &test.conf.ui), + As("test-float32", &test.conf.f32), + As("test-float64", &test.conf.f64), + As("test-duration", &test.conf.dur), + ); (err == nil) == test.expectErr { + t.Fatal("Failed to parse data:", err) + } + + if diff := cmp.Diff(test.want, test.conf, cmp.AllowUnexported(testConfig{})); diff != "" { + t.Fatal("(-want, +got)", diff) + } + }) + } +}