diff --git a/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured/helpers.go b/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured/helpers.go index 0f58d66c094..203c325d26c 100644 --- a/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured/helpers.go +++ b/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured/helpers.go @@ -125,6 +125,28 @@ func NestedInt64(obj map[string]interface{}, fields ...string) (int64, bool, err return i, true, nil } +// NestedNumberAsFloat64 returns the float64 value of a nested field. If the field's value is a +// float64, it is returned. If the field's value is an int64 that can be losslessly converted to +// float64, it will be converted and returned. Returns false if value is not found and an error if +// not a float64 or an int64 that can be accurately represented as a float64. +func NestedNumberAsFloat64(obj map[string]interface{}, fields ...string) (float64, bool, error) { + val, found, err := NestedFieldNoCopy(obj, fields...) + if !found || err != nil { + return 0, found, err + } + switch x := val.(type) { + case int64: + if x != int64(float64(x)) { + return 0, false, fmt.Errorf("%v accessor error: int64 value %v cannot be losslessly converted to float64", jsonPath(fields), x) + } + return float64(x), true, nil + case float64: + return x, true, nil + default: + return 0, false, fmt.Errorf("%v accessor error: %v is of the type %T, expected float64 or int64", jsonPath(fields), val, val) + } +} + // NestedStringSlice returns a copy of []string value of a nested field. // Returns false if value is not found and an error if not a []interface{} or contains non-string items in the slice. func NestedStringSlice(obj map[string]interface{}, fields ...string) ([]string, bool, error) { diff --git a/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured/helpers_test.go b/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured/helpers_test.go index f037e70351b..4394dd2eecb 100644 --- a/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured/helpers_test.go +++ b/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured/helpers_test.go @@ -18,6 +18,7 @@ package unstructured import ( "io/ioutil" + "math" "sync" "testing" @@ -225,3 +226,74 @@ func TestSetNestedMap(t *testing.T) { assert.Len(t, obj["x"].(map[string]interface{})["z"], 1) assert.Equal(t, "bar", obj["x"].(map[string]interface{})["z"].(map[string]interface{})["b"]) } + +func TestNestedNumberAsFloat64(t *testing.T) { + for _, tc := range []struct { + name string + obj map[string]interface{} + path []string + wantFloat64 float64 + wantBool bool + wantErrMessage string + }{ + { + name: "not found", + obj: nil, + path: []string{"missing"}, + wantFloat64: 0, + wantBool: false, + wantErrMessage: "", + }, + { + name: "found float64", + obj: map[string]interface{}{"value": float64(42)}, + path: []string{"value"}, + wantFloat64: 42, + wantBool: true, + wantErrMessage: "", + }, + { + name: "found unexpected type bool", + obj: map[string]interface{}{"value": true}, + path: []string{"value"}, + wantFloat64: 0, + wantBool: false, + wantErrMessage: ".value accessor error: true is of the type bool, expected float64 or int64", + }, + { + name: "found int64", + obj: map[string]interface{}{"value": int64(42)}, + path: []string{"value"}, + wantFloat64: 42, + wantBool: true, + wantErrMessage: "", + }, + { + name: "found int64 not representable as float64", + obj: map[string]interface{}{"value": int64(math.MaxInt64)}, + path: []string{"value"}, + wantFloat64: 0, + wantBool: false, + wantErrMessage: ".value accessor error: int64 value 9223372036854775807 cannot be losslessly converted to float64", + }, + } { + t.Run(tc.name, func(t *testing.T) { + gotFloat64, gotBool, gotErr := NestedNumberAsFloat64(tc.obj, tc.path...) + if gotFloat64 != tc.wantFloat64 { + t.Errorf("got %v, wanted %v", gotFloat64, tc.wantFloat64) + } + if gotBool != tc.wantBool { + t.Errorf("got %t, wanted %t", gotBool, tc.wantBool) + } + if tc.wantErrMessage != "" { + if gotErr == nil { + t.Errorf("got nil error, wanted %s", tc.wantErrMessage) + } else if gotErrMessage := gotErr.Error(); gotErrMessage != tc.wantErrMessage { + t.Errorf("wanted error %q, got: %v", gotErrMessage, tc.wantErrMessage) + } + } else if gotErr != nil { + t.Errorf("wanted nil error, got %v", gotErr) + } + }) + } +}