diff --git a/config/types/configuration.go b/config/types/configuration.go index 7fe30032..f90c5aaf 100644 --- a/config/types/configuration.go +++ b/config/types/configuration.go @@ -1,6 +1,8 @@ package types import ( + "bytes" + "encoding/json" "fmt" "os" "path/filepath" @@ -17,6 +19,55 @@ import ( // Configuration is the top-level container for all values for all services. See an example at: https://github.com/Azure/ARO-HCP/blob/main/config/config.yaml type Configuration map[string]any +// UnmarshalJSON decodes with UseNumber so JSON integers arrive as int64 +// instead of float64, preventing scientific notation for large numbers. +func (c *Configuration) UnmarshalJSON(data []byte) error { + var raw map[string]any + dec := json.NewDecoder(bytes.NewReader(data)) + dec.UseNumber() + if err := dec.Decode(&raw); err != nil { + return err + } + *c = normalizeNumbers(raw) + return nil +} + +func normalizeNumbers(m map[string]any) map[string]any { + for k, v := range m { + switch val := v.(type) { + case json.Number: + if i, err := val.Int64(); err == nil { + m[k] = i + } else if f, err := val.Float64(); err == nil { + m[k] = f + } + case map[string]any: + m[k] = normalizeNumbers(val) + case []any: + m[k] = normalizeNumbersSlice(val) + } + } + return m +} + +func normalizeNumbersSlice(s []any) []any { + for i, v := range s { + switch val := v.(type) { + case json.Number: + if n, err := val.Int64(); err == nil { + s[i] = n + } else if f, err := val.Float64(); err == nil { + s[i] = f + } + case map[string]any: + s[i] = normalizeNumbers(val) + case []any: + s[i] = normalizeNumbersSlice(val) + } + } + return s +} + type MissingKeyError struct { Path string Key string diff --git a/config/types/configuration_test.go b/config/types/configuration_test.go index d914635d..b2f83347 100644 --- a/config/types/configuration_test.go +++ b/config/types/configuration_test.go @@ -1,10 +1,136 @@ package types import ( + "fmt" "path/filepath" + "reflect" "testing" + + "sigs.k8s.io/yaml" ) +func TestConfigurationUnmarshalPreservesIntegerTypes(t *testing.T) { + type wrapper struct { + Defaults Configuration `json:"defaults"` + } + + tests := []struct { + name string + yaml string + path string + wantVal any + wantSprint string + }{ + { + name: "small integer", + yaml: "defaults:\n val: 1024", + path: "val", + wantVal: int64(1024), + wantSprint: "1024", + }, + { + name: "large integer no scientific notation", + yaml: "defaults:\n val: 2000000", + path: "val", + wantVal: int64(2000000), + wantSprint: "2000000", + }, + { + name: "zero", + yaml: "defaults:\n val: 0", + path: "val", + wantVal: int64(0), + wantSprint: "0", + }, + { + name: "negative integer", + yaml: "defaults:\n val: -42", + path: "val", + wantVal: int64(-42), + wantSprint: "-42", + }, + { + name: "float with decimal", + yaml: "defaults:\n val: 3.14", + path: "val", + wantVal: float64(3.14), + wantSprint: "3.14", + }, + { + name: "large float still uses scientific notation", + yaml: "defaults:\n val: 2000000.5", + path: "val", + wantVal: float64(2000000.5), + wantSprint: "2.0000005e+06", + }, + { + name: "string", + yaml: "defaults:\n val: hello", + path: "val", + wantVal: "hello", + wantSprint: "hello", + }, + { + name: "boolean", + yaml: "defaults:\n val: true", + path: "val", + wantVal: true, + wantSprint: "true", + }, + { + name: "nested integer", + yaml: "defaults:\n outer:\n inner: 4000000", + path: "outer.inner", + wantVal: int64(4000000), + wantSprint: "4000000", + }, + { + name: "deeply nested integer", + yaml: "defaults:\n a:\n b:\n c: 9999999", + path: "a.b.c", + wantVal: int64(9999999), + wantSprint: "9999999", + }, + { + name: "array with integers", + yaml: "defaults:\n val:\n - 1\n - 2000000\n - 3", + path: "val", + wantVal: []any{int64(1), int64(2000000), int64(3)}, + wantSprint: "[1 2000000 3]", + }, + { + name: "array with nested objects", + yaml: "defaults:\n val:\n - num: 2000000", + path: "val", + wantVal: []any{map[string]any{"num": int64(2000000)}}, + wantSprint: "[map[num:2000000]]", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var w wrapper + if err := yaml.Unmarshal([]byte(tt.yaml), &w); err != nil { + t.Fatalf("yaml.Unmarshal: %v", err) + } + + val, err := w.Defaults.GetByPath(tt.path) + if err != nil { + t.Fatalf("GetByPath(%q): %v", tt.path, err) + } + + if !reflect.DeepEqual(val, tt.wantVal) { + t.Errorf("value: got %v (%T), want %v (%T)", val, val, tt.wantVal, tt.wantVal) + } + + rendered := fmt.Sprint(val) + if rendered != tt.wantSprint { + t.Errorf("Sprint: got %q, want %q", rendered, tt.wantSprint) + } + }) + } +} + func TestResolveSchemaPath(t *testing.T) { tests := []struct { name string