Add NestedNumberAsFloat64 unstructured field accessor.

Go float64 values that have no fractional component and can be accurately represented as an int64,
when present in an unstructured object and roundtripped through JSON, appear in the resulting object
with the concrete type int64. For code that processes unstructured objects and expects to find
float64 values, this is a surprising edge case. NestedNumberAsFloat64 behaves the same as
NestedFloat64 when accessing a float64 value, but will additionally convert to float64 and return an
int64 value at the requested path. Errors are returned on encountering an int64 that cannot be
precisely represented as a float64.
This commit is contained in:
Ben Luddy 2024-10-03 13:19:31 -04:00
parent d99d3f7eb7
commit 30c35a5618
No known key found for this signature in database
GPG Key ID: A6551E73A5974C30
2 changed files with 94 additions and 0 deletions

View File

@ -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) {

View File

@ -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)
}
})
}
}