Skip to content
Closed
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
38 changes: 35 additions & 3 deletions deployment/cre/jobs/types/job_spec.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package job_types

import (
"encoding/json"
"errors"
"fmt"
"strings"
Expand All @@ -13,14 +14,45 @@ import (
type JobSpecInput map[string]any

func (j JobSpecInput) UnmarshalTo(target any) error {
bytes, err := yaml.Marshal(j)
sanitized := convertJSONNumbers(map[string]any(j))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if this is going to work... at this stage we already lost int types in the nested map[string]any array

bytes, err := yaml.Marshal(sanitized)
if err != nil {
return fmt.Errorf("failed to marshal job spec input to json: %w", err)
return fmt.Errorf("failed to marshal job spec input to yaml: %w", err)
}

return yaml.Unmarshal(bytes, target)
}

func convertJSONNumbers(m map[string]any) map[string]any {
out := make(map[string]any, len(m))
for k, v := range m {
out[k] = convertValue(v)
}
return out
}

func convertValue(v any) any {
switch val := v.(type) {
case json.Number:
if i, err := val.Int64(); err == nil {
return i
}
if f, err := val.Float64(); err == nil {
return f
}
return val.String()
case map[string]any:
return convertJSONNumbers(val)
case []any:
out := make([]any, len(val))
for i, item := range val {
out[i] = convertValue(item)
}
return out
default:
return v
}
}

func (j JobSpecInput) UnmarshalFrom(source any) error {
bytes, err := yaml.Marshal(source)
if err != nil {
Expand Down
66 changes: 66 additions & 0 deletions deployment/cre/jobs/types/job_spec_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package job_types_test

import (
"encoding/json"
"testing"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -120,3 +121,68 @@ func TestJobSpecInput_ToStandardCapabilityJob(t *testing.T) {
assert.Contains(t, err.Error(), "cannot unmarshal !!str")
})
}

func TestUnmarshalTo_JSONNumberIntFields(t *testing.T) {
t.Parallel()

type DON struct {
Name string `yaml:"name"`
F int `yaml:"f"`
Handlers []string `yaml:"handlers"`
}

type GatewayInput struct {
GatewayRequestTimeoutSec int `yaml:"gatewayRequestTimeoutSec"`
DONs []DON `yaml:"dons"`
SomeString string `yaml:"someString"`
}

// Simulate the exact values that arrive after the YAML->JSON->UseNumber() pipeline.
// In the real pipeline, YamlNodeToAny converts YAML integers to json.Number,
// then json.Marshal -> env var -> json.Decoder.UseNumber() preserves them as json.Number.
input := job_types.JobSpecInput{
"gatewayRequestTimeoutSec": json.Number("70"),
"someString": "hello",
"dons": []any{
map[string]any{
"name": "some_don",
"f": json.Number("1"),
"handlers": []any{"http-capabilities", "web-api-capabilities"},
},
},
}

var target GatewayInput
err := input.UnmarshalTo(&target)
require.NoError(t, err, "UnmarshalTo should handle json.Number values in int fields")

assert.Equal(t, 70, target.GatewayRequestTimeoutSec)
assert.Equal(t, "hello", target.SomeString)
require.Len(t, target.DONs, 1)
assert.Equal(t, "some_don", target.DONs[0].Name)
assert.Equal(t, 1, target.DONs[0].F)
assert.Equal(t, []string{"http-capabilities", "web-api-capabilities"}, target.DONs[0].Handlers)
}

// TestUnmarshalTo_NativeIntFields verifies the happy path where map values
// are already native Go ints (e.g. when constructed in Go code rather than
// through the JSON pipeline). This should always pass.
func TestUnmarshalTo_NativeIntFields(t *testing.T) {
t.Parallel()

type Target struct {
Count int `yaml:"count"`
Message string `yaml:"message"`
}

input := job_types.JobSpecInput{
"count": 42,
"message": "test",
}

var target Target
err := input.UnmarshalTo(&target)
require.NoError(t, err)
assert.Equal(t, 42, target.Count)
assert.Equal(t, "test", target.Message)
}
Loading