Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions config/types/configuration.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package types

import (
"bytes"
"encoding/json"
"fmt"
"os"
"path/filepath"
Expand All @@ -26,6 +28,51 @@ func (e MissingKeyError) Error() string {
return fmt.Sprintf("configuration%s: key %s not found", e.Path, e.Key)
}

// UnmarshalJSON overrides the default JSON unmarshaling to preserve integer
// precision. Without it, all numbers become float64 and values >= 1e6 render
// as scientific notation in Go templates (e.g. "2e+06").
func (c *Configuration) UnmarshalJSON(data []byte) error {
dec := json.NewDecoder(bytes.NewReader(data))
dec.UseNumber()
var raw map[string]any
if err := dec.Decode(&raw); err != nil {
return err
}
if raw == nil {
*c = nil
return nil
}
*c = Configuration(convertJSONNumbers(raw).(map[string]any))
return nil
}

// convertJSONNumbers recursively walks a decoded JSON value and converts
// json.Number to int64 (whole numbers) or float64 (fractional).
func convertJSONNumbers(v any) any {
switch val := v.(type) {
case map[string]any:
for k, inner := range val {
val[k] = convertJSONNumbers(inner)
}
return val
case []any:
for i, inner := range val {
val[i] = convertJSONNumbers(inner)
}
return val
case json.Number:
if i, err := val.Int64(); err == nil {
return i
}
if f, err := val.Float64(); err == nil {
return f
}
return val.String()
default:
return val
}
}

func (v Configuration) GetByPath(path string) (any, error) {
keys := strings.Split(path, ".")
var current any = map[string]any(v)
Expand Down
66 changes: 66 additions & 0 deletions config/types/configuration_test.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,76 @@
package types

import (
"bytes"
"encoding/json"
"path/filepath"
"testing"
"text/template"

"sigs.k8s.io/yaml"
)

func TestConfiguration_UnmarshalJSON_NestedAndArrays(t *testing.T) {
input := `{
"nested": {"count": 5000000},
"items": [3000000, 1.5, "text"]
}`

var cfg Configuration
if err := json.Unmarshal([]byte(input), &cfg); err != nil {
t.Fatalf("UnmarshalJSON failed: %v", err)
}

nested, ok := cfg["nested"].(map[string]any)
if !ok {
t.Fatalf("nested is %T, want map[string]any", cfg["nested"])
}
if v := nested["count"]; v != int64(5000000) {
t.Errorf("nested.count = %v (%T), want int64(5000000)", v, v)
}

items, ok := cfg["items"].([]any)
if !ok {
t.Fatalf("items is %T, want []any", cfg["items"])
}
if items[0] != int64(3000000) {
t.Errorf("items[0] = %v (%T), want int64(3000000)", items[0], items[0])
}
if items[1] != float64(1.5) {
t.Errorf("items[1] = %v (%T), want float64(1.5)", items[1], items[1])
}
if items[2] != "text" {
t.Errorf("items[2] = %v (%T), want \"text\"", items[2], items[2])
}
}

func TestConfiguration_UnmarshalJSON_Null(t *testing.T) {
var cfg Configuration
if err := json.Unmarshal([]byte("null"), &cfg); err != nil {
t.Fatalf("UnmarshalJSON failed: %v", err)
}
if cfg != nil {
t.Fatalf("cfg = %#v, want nil", cfg)
}
}

func TestConfiguration_TemplateRendering_NoScientificNotation(t *testing.T) {
input := `largeInt: 2000000`
var cfg Configuration
if err := yaml.Unmarshal([]byte(input), &cfg); err != nil {
t.Fatalf("yaml.Unmarshal failed: %v", err)
}

tmpl := template.Must(template.New("t").Parse("{{ .largeInt }}"))
var buf bytes.Buffer
if err := tmpl.Execute(&buf, map[string]any(cfg)); err != nil {
t.Fatalf("template.Execute failed: %v", err)
}
if got := buf.String(); got != "2000000" {
t.Errorf("template rendered %q, want %q", got, "2000000")
}
}

func TestResolveSchemaPath(t *testing.T) {
tests := []struct {
name string
Expand Down