diff --git a/pkg/api/params.go b/pkg/api/params.go new file mode 100644 index 000000000..85176b1e4 --- /dev/null +++ b/pkg/api/params.go @@ -0,0 +1,28 @@ +package api + +import "fmt" + +// ErrInvalidInt64Type is returned when a value cannot be converted to int64. +type ErrInvalidInt64Type struct { + Value interface{} +} + +func (e *ErrInvalidInt64Type) Error() string { + return fmt.Sprintf("expected integer, got %T", e.Value) +} + +// ParseInt64 converts an interface{} value (typically from JSON-decoded tool arguments) +// to int64. Handles float64 (JSON numbers), int, and int64 types. +// Returns the converted value and nil error on success, or 0 and an ErrInvalidInt64Type if the type is unsupported. +func ParseInt64(value interface{}) (int64, error) { + switch v := value.(type) { + case float64: + return int64(v), nil + case int: + return int64(v), nil + case int64: + return v, nil + default: + return 0, &ErrInvalidInt64Type{Value: value} + } +} diff --git a/pkg/api/params_test.go b/pkg/api/params_test.go new file mode 100644 index 000000000..c2d806034 --- /dev/null +++ b/pkg/api/params_test.go @@ -0,0 +1,105 @@ +package api + +import ( + "testing" + + "github.com/stretchr/testify/suite" +) + +type ParamsSuite struct { + suite.Suite +} + +func TestParamsSuite(t *testing.T) { + suite.Run(t, new(ParamsSuite)) +} + +func (s *ParamsSuite) TestParseInt64() { + s.Run("float64 value is converted to int64", func() { + result, err := ParseInt64(float64(42.0)) + s.NoError(err) + s.Equal(int64(42), result) + }) + + s.Run("float64 with decimal truncates to int64", func() { + result, err := ParseInt64(float64(42.9)) + s.NoError(err) + s.Equal(int64(42), result) + }) + + s.Run("int value is converted to int64", func() { + result, err := ParseInt64(int(100)) + s.NoError(err) + s.Equal(int64(100), result) + }) + + s.Run("int64 value is returned as-is", func() { + result, err := ParseInt64(int64(999)) + s.NoError(err) + s.Equal(int64(999), result) + }) + + s.Run("negative float64 value is converted correctly", func() { + result, err := ParseInt64(float64(-10.0)) + s.NoError(err) + s.Equal(int64(-10), result) + }) + + s.Run("negative int value is converted correctly", func() { + result, err := ParseInt64(int(-5)) + s.NoError(err) + s.Equal(int64(-5), result) + }) + + s.Run("zero value is handled correctly", func() { + result, err := ParseInt64(float64(0)) + s.NoError(err) + s.Equal(int64(0), result) + }) + + s.Run("string value returns error", func() { + result, err := ParseInt64("not a number") + s.Error(err) + s.Equal(int64(0), result) + s.Contains(err.Error(), "string") + }) + + s.Run("nil value returns error", func() { + result, err := ParseInt64(nil) + s.Error(err) + s.Equal(int64(0), result) + }) + + s.Run("bool value returns error", func() { + result, err := ParseInt64(true) + s.Error(err) + s.Equal(int64(0), result) + s.Contains(err.Error(), "bool") + }) + + s.Run("slice value returns error", func() { + result, err := ParseInt64([]int{1, 2, 3}) + s.Error(err) + s.Equal(int64(0), result) + }) + + s.Run("map value returns error", func() { + result, err := ParseInt64(map[string]int{"a": 1}) + s.Error(err) + s.Equal(int64(0), result) + }) +} + +func (s *ParamsSuite) TestErrInvalidInt64Type() { + s.Run("error includes type information", func() { + err := &ErrInvalidInt64Type{Value: "test"} + s.Contains(err.Error(), "string") + }) + + s.Run("error can be type asserted", func() { + _, err := ParseInt64("invalid") + var typeErr *ErrInvalidInt64Type + s.ErrorAs(err, &typeErr) + s.Equal("invalid", typeErr.Value) + }) +} diff --git a/pkg/toolsets/core/nodes.go b/pkg/toolsets/core/nodes.go index e42a8a988..1d070a8f3 100644 --- a/pkg/toolsets/core/nodes.go +++ b/pkg/toolsets/core/nodes.go @@ -108,15 +108,10 @@ func nodesLog(params api.ToolHandlerParams) (*api.ToolCallResult, error) { tailLines := params.GetArguments()["tailLines"] var tailInt int64 if tailLines != nil { - // Convert to int64 - safely handle both float64 (JSON number) and int types - switch v := tailLines.(type) { - case float64: - tailInt = int64(v) - case int: - case int64: - tailInt = v - default: - return api.NewToolCallResult("", fmt.Errorf("failed to parse tail parameter: expected integer, got %T", tailLines)), nil + var err error + tailInt, err = api.ParseInt64(tailLines) + if err != nil { + return api.NewToolCallResult("", fmt.Errorf("failed to parse tailLines parameter: %w", err)), nil } } ret, err := params.NodesLog(params, name, query, tailInt) diff --git a/pkg/toolsets/core/pods.go b/pkg/toolsets/core/pods.go index 787813324..f19886866 100644 --- a/pkg/toolsets/core/pods.go +++ b/pkg/toolsets/core/pods.go @@ -397,16 +397,10 @@ func podsLog(params api.ToolHandlerParams) (*api.ToolCallResult, error) { tail := params.GetArguments()["tail"] var tailInt int64 if tail != nil { - // Convert to int64 - safely handle both float64 (JSON number) and int types - switch v := tail.(type) { - case float64: - tailInt = int64(v) - case int: - tailInt = int64(v) - case int64: - tailInt = v - default: - return api.NewToolCallResult("", fmt.Errorf("failed to parse tail parameter: expected integer, got %T", tail)), nil + var err error + tailInt, err = api.ParseInt64(tail) + if err != nil { + return api.NewToolCallResult("", fmt.Errorf("failed to parse tail parameter: %w", err)), nil } } diff --git a/pkg/toolsets/core/resources.go b/pkg/toolsets/core/resources.go index 916b0bfcd..33b92bfa4 100644 --- a/pkg/toolsets/core/resources.go +++ b/pkg/toolsets/core/resources.go @@ -346,16 +346,11 @@ func resourcesScale(params api.ToolHandlerParams) (*api.ToolCallResult, error) { } func parseScaleValue(desiredScale interface{}) (int64, error) { - switch s := desiredScale.(type) { - case float64: - return int64(s), nil - case int: - return int64(s), nil - case int64: - return s, nil - default: - return 0, fmt.Errorf("failed to parse scale parameter: expected integer, got %T", desiredScale) + v, err := api.ParseInt64(desiredScale) + if err != nil { + return 0, fmt.Errorf("failed to parse scale parameter: %w", err) } + return v, nil } func parseGroupVersionKind(arguments map[string]interface{}) (*schema.GroupVersionKind, error) { diff --git a/pkg/toolsets/kiali/logs.go b/pkg/toolsets/kiali/logs.go index 5c74ce689..8c9521c08 100644 --- a/pkg/toolsets/kiali/logs.go +++ b/pkg/toolsets/kiali/logs.go @@ -86,14 +86,11 @@ func workloadLogsHandler(params api.ToolHandlerParams) (*api.ToolCallResult, err // Convert tail to maxLines if tail != nil { - switch v := tail.(type) { - case float64: - maxLines = fmt.Sprintf("%.0f", v) - case int: - maxLines = fmt.Sprintf("%d", v) - case int64: - maxLines = fmt.Sprintf("%d", v) + tailInt, err := api.ParseInt64(tail) + if err != nil { + return api.NewToolCallResult("", fmt.Errorf("failed to parse tail parameter: %w", err)), nil } + maxLines = fmt.Sprintf("%d", tailInt) } // If no container specified, we need to get workload details first to find the main app container