Skip to content

Commit 30c35a5

Browse files
committed
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.
1 parent d99d3f7 commit 30c35a5

File tree

2 files changed

+94
-0
lines changed

2 files changed

+94
-0
lines changed

staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured/helpers.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,28 @@ func NestedInt64(obj map[string]interface{}, fields ...string) (int64, bool, err
125125
return i, true, nil
126126
}
127127

128+
// NestedNumberAsFloat64 returns the float64 value of a nested field. If the field's value is a
129+
// float64, it is returned. If the field's value is an int64 that can be losslessly converted to
130+
// float64, it will be converted and returned. Returns false if value is not found and an error if
131+
// not a float64 or an int64 that can be accurately represented as a float64.
132+
func NestedNumberAsFloat64(obj map[string]interface{}, fields ...string) (float64, bool, error) {
133+
val, found, err := NestedFieldNoCopy(obj, fields...)
134+
if !found || err != nil {
135+
return 0, found, err
136+
}
137+
switch x := val.(type) {
138+
case int64:
139+
if x != int64(float64(x)) {
140+
return 0, false, fmt.Errorf("%v accessor error: int64 value %v cannot be losslessly converted to float64", jsonPath(fields), x)
141+
}
142+
return float64(x), true, nil
143+
case float64:
144+
return x, true, nil
145+
default:
146+
return 0, false, fmt.Errorf("%v accessor error: %v is of the type %T, expected float64 or int64", jsonPath(fields), val, val)
147+
}
148+
}
149+
128150
// NestedStringSlice returns a copy of []string value of a nested field.
129151
// Returns false if value is not found and an error if not a []interface{} or contains non-string items in the slice.
130152
func NestedStringSlice(obj map[string]interface{}, fields ...string) ([]string, bool, error) {

staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured/helpers_test.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package unstructured
1818

1919
import (
2020
"io/ioutil"
21+
"math"
2122
"sync"
2223
"testing"
2324

@@ -225,3 +226,74 @@ func TestSetNestedMap(t *testing.T) {
225226
assert.Len(t, obj["x"].(map[string]interface{})["z"], 1)
226227
assert.Equal(t, "bar", obj["x"].(map[string]interface{})["z"].(map[string]interface{})["b"])
227228
}
229+
230+
func TestNestedNumberAsFloat64(t *testing.T) {
231+
for _, tc := range []struct {
232+
name string
233+
obj map[string]interface{}
234+
path []string
235+
wantFloat64 float64
236+
wantBool bool
237+
wantErrMessage string
238+
}{
239+
{
240+
name: "not found",
241+
obj: nil,
242+
path: []string{"missing"},
243+
wantFloat64: 0,
244+
wantBool: false,
245+
wantErrMessage: "",
246+
},
247+
{
248+
name: "found float64",
249+
obj: map[string]interface{}{"value": float64(42)},
250+
path: []string{"value"},
251+
wantFloat64: 42,
252+
wantBool: true,
253+
wantErrMessage: "",
254+
},
255+
{
256+
name: "found unexpected type bool",
257+
obj: map[string]interface{}{"value": true},
258+
path: []string{"value"},
259+
wantFloat64: 0,
260+
wantBool: false,
261+
wantErrMessage: ".value accessor error: true is of the type bool, expected float64 or int64",
262+
},
263+
{
264+
name: "found int64",
265+
obj: map[string]interface{}{"value": int64(42)},
266+
path: []string{"value"},
267+
wantFloat64: 42,
268+
wantBool: true,
269+
wantErrMessage: "",
270+
},
271+
{
272+
name: "found int64 not representable as float64",
273+
obj: map[string]interface{}{"value": int64(math.MaxInt64)},
274+
path: []string{"value"},
275+
wantFloat64: 0,
276+
wantBool: false,
277+
wantErrMessage: ".value accessor error: int64 value 9223372036854775807 cannot be losslessly converted to float64",
278+
},
279+
} {
280+
t.Run(tc.name, func(t *testing.T) {
281+
gotFloat64, gotBool, gotErr := NestedNumberAsFloat64(tc.obj, tc.path...)
282+
if gotFloat64 != tc.wantFloat64 {
283+
t.Errorf("got %v, wanted %v", gotFloat64, tc.wantFloat64)
284+
}
285+
if gotBool != tc.wantBool {
286+
t.Errorf("got %t, wanted %t", gotBool, tc.wantBool)
287+
}
288+
if tc.wantErrMessage != "" {
289+
if gotErr == nil {
290+
t.Errorf("got nil error, wanted %s", tc.wantErrMessage)
291+
} else if gotErrMessage := gotErr.Error(); gotErrMessage != tc.wantErrMessage {
292+
t.Errorf("wanted error %q, got: %v", gotErrMessage, tc.wantErrMessage)
293+
}
294+
} else if gotErr != nil {
295+
t.Errorf("wanted nil error, got %v", gotErr)
296+
}
297+
})
298+
}
299+
}

0 commit comments

Comments
 (0)