feat: Add Map parameters support (#928)

support both generic and typed map. Config example:
```
 parameters:
      - name: user_scores
        type: map
        description: A map of user IDs to their scores. All scores must be integers.
        valueType: integer # This enforces the value type for all entries. Leave it blank for generic map

```
Represented as `Object` with `additionalProperties` in manifests.

Added a util function to convert json.Number (string type) to int/float
types to address the problem where int/float values are converted to
strings for the generic map.
This commit is contained in:
Wenxin Du
2025-07-18 17:19:09 -04:00
committed by GitHub
parent 9a55b80482
commit 4468bc920b
4 changed files with 551 additions and 64 deletions

View File

@@ -115,6 +115,38 @@ in the list using the items field:
Items in array should not have a default value. If provided, it will be ignored.
{{< /notice >}}
### Map Parameters
The map type is a collection of key-value pairs. It can be configured in two ways:
- Generic Map: By default, it accepts values of any primitive type (string, number, boolean), allowing for mixed data.
- Typed Map: By setting the valueType field, you can enforce that all values
within the map must be of the same specified type.
#### Generic Map (Mixed Value Types)
This is the default behavior when valueType is omitted. It's useful for passing a flexible group of settings.
```yaml
parameters:
- name: execution_context
type: map
description: A flexible set of key-value pairs for the execution environment.
```
#### Typed Map
Specify valueType to ensure all values in the map are of the same type. An error
will be thrown in case of value type mismatch.
```yaml
parameters:
- name: user_scores
type: map
description: A map of user IDs to their scores. All scores must be integers.
valueType: integer # This enforces the value type for all entries.
```
### Authenticated Parameters
Authenticated parameters are automatically populated with user

View File

@@ -32,6 +32,7 @@ const (
typeFloat = "float"
typeBool = "boolean"
typeArray = "array"
typeMap = "map"
)
// ParamValues is an ordered list of ParamValue
@@ -367,6 +368,17 @@ func parseParamFromDelayedUnmarshaler(ctx context.Context, u *util.DelayedUnmars
a.AuthSources = nil
}
return a, nil
case typeMap:
a := &MapParameter{}
if err := dec.DecodeContext(ctx, a); err != nil {
return nil, fmt.Errorf("unable to parse as %q: %w", t, err)
}
if a.AuthSources != nil {
logger.WarnContext(ctx, "`authSources` is deprecated, use `authServices` for parameters instead")
a.AuthServices = append(a.AuthServices, a.AuthSources...)
a.AuthSources = nil
}
return a, nil
}
return nil, fmt.Errorf("%q is not valid type for a parameter", t)
}
@@ -401,19 +413,21 @@ func (ps Parameters) McpManifest() McpToolsSchema {
// ParameterManifest represents parameters when served as part of a ToolManifest.
type ParameterManifest struct {
Name string `json:"name"`
Type string `json:"type"`
Required bool `json:"required"`
Description string `json:"description"`
AuthServices []string `json:"authSources"`
Items *ParameterManifest `json:"items,omitempty"`
Name string `json:"name"`
Type string `json:"type"`
Required bool `json:"required"`
Description string `json:"description"`
AuthServices []string `json:"authSources"`
Items *ParameterManifest `json:"items,omitempty"`
AdditionalProperties any `json:"AdditionalProperties,omitempty"`
}
// ParameterMcpManifest represents properties when served as part of a ToolMcpManifest.
type ParameterMcpManifest struct {
Type string `json:"type"`
Description string `json:"description"`
Items *ParameterMcpManifest `json:"items,omitempty"`
Type string `json:"type"`
Description string `json:"description"`
Items *ParameterMcpManifest `json:"items,omitempty"`
AdditionalProperties any `json:"AdditionalProperties,omitempty"`
}
// CommonParameter are default fields that are emebdding in most Parameter implementations. Embedding this stuct will give the object Name() and Type() functions.
@@ -1022,3 +1036,211 @@ func (p *ArrayParameter) McpManifest() ParameterMcpManifest {
Items: &items,
}
}
// MapParameter is a parameter representing a map with string keys. If ValueType is
// specified (e.g., "string"), values are validated against that type. If ValueType
// is empty, it is treated as a generic map[string]any.
type MapParameter struct {
CommonParameter `yaml:",inline"`
Default *map[string]any `yaml:"default,omitempty"`
ValueType string `yaml:"valueType,omitempty"`
}
// Ensure MapParameter implements the Parameter interface.
var _ Parameter = &MapParameter{}
// NewMapParameter is a convenience function for initializing a MapParameter.
func NewMapParameter(name string, desc string, valueType string) *MapParameter {
return &MapParameter{
CommonParameter: CommonParameter{
Name: name,
Type: "map",
Desc: desc,
},
ValueType: valueType,
}
}
// NewMapParameterWithDefault is a convenience function for initializing a MapParameter with a default value.
func NewMapParameterWithDefault(name string, defaultV map[string]any, desc string, valueType string) *MapParameter {
return &MapParameter{
CommonParameter: CommonParameter{
Name: name,
Type: "map",
Desc: desc,
},
ValueType: valueType,
Default: &defaultV,
}
}
// NewMapParameterWithRequired is a convenience function for initializing a MapParameter as required.
func NewMapParameterWithRequired(name string, desc string, required bool, valueType string) *MapParameter {
return &MapParameter{
CommonParameter: CommonParameter{
Name: name,
Type: "map",
Desc: desc,
Required: &required,
},
ValueType: valueType,
}
}
// NewMapParameterWithAuth is a convenience function for initializing a MapParameter with auth services.
func NewMapParameterWithAuth(name string, desc string, valueType string, authServices []ParamAuthService) *MapParameter {
return &MapParameter{
CommonParameter: CommonParameter{
Name: name,
Type: "map",
Desc: desc,
AuthServices: authServices,
},
ValueType: valueType,
}
}
// UnmarshalYAML handles parsing the MapParameter from YAML input.
func (p *MapParameter) UnmarshalYAML(ctx context.Context, unmarshal func(interface{}) error) error {
var rawItem struct {
CommonParameter `yaml:",inline"`
Default *map[string]any `yaml:"default"`
ValueType string `yaml:"valueType"`
}
if err := unmarshal(&rawItem); err != nil {
return err
}
// Validate `ValueType` to be one of the supported basic types
if rawItem.ValueType != "" {
if _, err := getPrototypeParameter(rawItem.ValueType); err != nil {
return err
}
}
p.CommonParameter = rawItem.CommonParameter
p.Default = rawItem.Default
p.ValueType = rawItem.ValueType
return nil
}
// getPrototypeParameter is a helper factory to create a temporary parameter
// based on a type string for parsing and manifest generation.
func getPrototypeParameter(typeName string) (Parameter, error) {
switch typeName {
case "string":
return NewStringParameter("", ""), nil
case "integer":
return NewIntParameter("", ""), nil
case "boolean":
return NewBooleanParameter("", ""), nil
case "float":
return NewFloatParameter("", ""), nil
default:
return nil, fmt.Errorf("unsupported valueType %q for map parameter", typeName)
}
}
// Parse validates and parses an incoming value for the map parameter.
func (p *MapParameter) Parse(v any) (any, error) {
m, ok := v.(map[string]any)
if !ok {
return nil, &ParseTypeError{p.Name, p.Type, m}
}
// for generic maps, convert json.Numbers to their corresponding types
if p.ValueType == "" {
convertedData, err := util.ConvertNumbers(m)
if err != nil {
return nil, fmt.Errorf("failed to parse integer or float values in map: %s", err)
}
convertedMap, ok := convertedData.(map[string]any)
if !ok {
return nil, fmt.Errorf("internal error: ConvertNumbers should return a map, but got type %T", convertedData)
}
return convertedMap, nil
}
// Otherwise, get a prototype and parse each value in the map.
prototype, err := getPrototypeParameter(p.ValueType)
if err != nil {
return nil, err
}
rtn := make(map[string]any, len(m))
for key, val := range m {
parsedVal, err := prototype.Parse(val)
if err != nil {
return nil, fmt.Errorf("unable to parse value for key %q: %w", key, err)
}
rtn[key] = parsedVal
}
return rtn, nil
}
func (p *MapParameter) GetAuthServices() []ParamAuthService {
return p.AuthServices
}
func (p *MapParameter) GetDefault() any {
if p.Default == nil {
return nil
}
return *p.Default
}
func (p *MapParameter) GetValueType() string {
return p.ValueType
}
// Manifest returns the manifest for the MapParameter.
func (p *MapParameter) Manifest() ParameterManifest {
authNames := make([]string, len(p.AuthServices))
for i, a := range p.AuthServices {
authNames[i] = a.Name
}
r := CheckParamRequired(p.GetRequired(), p.GetDefault())
var additionalProperties any
if p.ValueType != "" {
prototype, err := getPrototypeParameter(p.ValueType)
if err != nil {
panic(err)
}
valueSchema := prototype.Manifest()
additionalProperties = &valueSchema
} else {
// If no valueType is given, allow any properties.
additionalProperties = true
}
return ParameterManifest{
Name: p.Name,
Type: "object",
Required: r,
Description: p.Desc,
AuthServices: authNames,
AdditionalProperties: additionalProperties,
}
}
// McpManifest returns the MCP manifest for the MapParameter.
func (p *MapParameter) McpManifest() ParameterMcpManifest {
var additionalProperties any
if p.ValueType != "" {
prototype, err := getPrototypeParameter(p.ValueType)
if err != nil {
panic(err)
}
valueSchema := prototype.McpManifest()
additionalProperties = &valueSchema
} else {
// If no valueType is given, allow any properties.
additionalProperties = true
}
return ParameterMcpManifest{
Type: "object",
Description: p.Desc,
AdditionalProperties: additionalProperties,
}
}

View File

@@ -18,10 +18,10 @@ import (
"bytes"
"encoding/json"
"math"
"reflect"
"strings"
"testing"
yaml "github.com/goccy/go-yaml"
"github.com/goccy/go-yaml"
"github.com/google/go-cmp/cmp"
"github.com/googleapis/genai-toolbox/internal/testutils"
"github.com/googleapis/genai-toolbox/internal/tools"
@@ -294,6 +294,63 @@ func TestParametersMarshal(t *testing.T) {
tools.NewArrayParameterWithDefault("my_array", []any{1.0, 1.1}, "this param is an array of floats", tools.NewFloatParameter("my_float", "float item")),
},
},
{
name: "map with string values",
in: []map[string]any{
{
"name": "my_map",
"type": "map",
"description": "this param is a map of strings",
"valueType": "string",
},
},
want: tools.Parameters{
tools.NewMapParameter("my_map", "this param is a map of strings", "string"),
},
},
{
name: "map not required",
in: []map[string]any{
{
"name": "my_map",
"type": "map",
"description": "this param is a map of strings",
"required": false,
"valueType": "string",
},
},
want: tools.Parameters{
tools.NewMapParameterWithRequired("my_map", "this param is a map of strings", false, "string"),
},
},
{
name: "map with default",
in: []map[string]any{
{
"name": "my_map",
"type": "map",
"description": "this param is a map of strings",
"default": map[string]any{"key1": "val1"},
"valueType": "string",
},
},
want: tools.Parameters{
tools.NewMapParameterWithDefault("my_map", map[string]any{"key1": "val1"}, "this param is a map of strings", "string"),
},
},
{
name: "generic map (no valueType)",
in: []map[string]any{
{
"name": "my_generic_map",
"type": "map",
"description": "this param is a generic map",
},
},
want: tools.Parameters{
tools.NewMapParameter("my_generic_map", "this param is a generic map", ""),
},
},
}
for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
@@ -350,13 +407,13 @@ func TestAuthParametersMarshal(t *testing.T) {
},
},
{
name: "string with authSources",
name: "string with authServices",
in: []map[string]any{
{
"name": "my_string",
"type": "string",
"description": "this param is a string",
"authSources": []map[string]string{
"authServices": []map[string]string{
{
"name": "my-google-auth-service",
"field": "user_id",
@@ -396,13 +453,13 @@ func TestAuthParametersMarshal(t *testing.T) {
},
},
{
name: "int with authSources",
name: "int with authServices",
in: []map[string]any{
{
"name": "my_integer",
"type": "integer",
"description": "this param is an int",
"authSources": []map[string]string{
"authServices": []map[string]string{
{
"name": "my-google-auth-service",
"field": "user_id",
@@ -442,13 +499,13 @@ func TestAuthParametersMarshal(t *testing.T) {
},
},
{
name: "float with authSources",
name: "float with authServices",
in: []map[string]any{
{
"name": "my_float",
"type": "float",
"description": "my param is a float",
"authSources": []map[string]string{
"authServices": []map[string]string{
{
"name": "my-google-auth-service",
"field": "user_id",
@@ -488,13 +545,13 @@ func TestAuthParametersMarshal(t *testing.T) {
},
},
{
name: "bool with authSources",
name: "bool with authServices",
in: []map[string]any{
{
"name": "my_bool",
"type": "boolean",
"description": "this param is a boolean",
"authSources": []map[string]string{
"authServices": []map[string]string{
{
"name": "my-google-auth-service",
"field": "user_id",
@@ -539,7 +596,7 @@ func TestAuthParametersMarshal(t *testing.T) {
},
},
{
name: "string array with authSources",
name: "string array with authServices",
in: []map[string]any{
{
"name": "my_array",
@@ -550,7 +607,7 @@ func TestAuthParametersMarshal(t *testing.T) {
"type": "string",
"description": "string item",
},
"authSources": []map[string]string{
"authServices": []map[string]string{
{
"name": "my-google-auth-service",
"field": "user_id",
@@ -594,6 +651,24 @@ func TestAuthParametersMarshal(t *testing.T) {
tools.NewArrayParameterWithAuth("my_array", "this param is an array of floats", tools.NewFloatParameter("my_float", "float item"), authServices),
},
},
{
name: "map",
in: []map[string]any{
{
"name": "my_map",
"type": "map",
"description": "this param is a map of strings",
"valueType": "string",
"authServices": []map[string]string{
{"name": "my-google-auth-service", "field": "user_id"},
{"name": "other-auth-service", "field": "user_id"},
},
},
},
want: tools.Parameters{
tools.NewMapParameterWithAuth("my_map", "this param is a map of strings", "string", authServices),
},
},
}
for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
@@ -622,6 +697,7 @@ func TestParametersParse(t *testing.T) {
in map[string]any
want tools.ParamValues
}{
// ... (primitive type tests are unchanged)
{
name: "string",
params: tools.Parameters{
@@ -780,6 +856,51 @@ func TestParametersParse(t *testing.T) {
in: map[string]any{},
want: tools.ParamValues{tools.ParamValue{Name: "my_bool", Value: nil}},
},
{
name: "map",
params: tools.Parameters{
tools.NewMapParameter("my_map", "a map", "string"),
},
in: map[string]any{
"my_map": map[string]any{"key1": "val1", "key2": "val2"},
},
want: tools.ParamValues{tools.ParamValue{Name: "my_map", Value: map[string]any{"key1": "val1", "key2": "val2"}}},
},
{
name: "generic map",
params: tools.Parameters{
tools.NewMapParameter("my_map_generic_type", "a generic map", ""),
},
in: map[string]any{
"my_map_generic_type": map[string]any{"key1": "val1", "key2": 123, "key3": true},
},
want: tools.ParamValues{tools.ParamValue{Name: "my_map_generic_type", Value: map[string]any{"key1": "val1", "key2": int64(123), "key3": true}}},
},
{
name: "not map (value type mismatch)",
params: tools.Parameters{
tools.NewMapParameter("my_map", "a map", "string"),
},
in: map[string]any{
"my_map": map[string]any{"key1": 123},
},
},
{
name: "map default",
params: tools.Parameters{
tools.NewMapParameterWithDefault("my_map_default", map[string]any{"default_key": "default_val"}, "a map", "string"),
},
in: map[string]any{},
want: tools.ParamValues{tools.ParamValue{Name: "my_map_default", Value: map[string]any{"default_key": "default_val"}}},
},
{
name: "map not required",
params: tools.Parameters{
tools.NewMapParameterWithRequired("my_map_not_required", "a map", false, "string"),
},
in: map[string]any{},
want: tools.ParamValues{tools.ParamValue{Name: "my_map_not_required", Value: nil}},
},
}
for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
@@ -809,15 +930,10 @@ func TestParametersParse(t *testing.T) {
if wantErr {
t.Fatalf("expected error but Param parsed successfully: %s", gotAll)
}
for i, got := range gotAll {
want := tc.want[i]
if got != want {
t.Fatalf("unexpected value: got %q, want %q", got, want)
}
gotType, wantType := reflect.TypeOf(got), reflect.TypeOf(want)
if gotType != wantType {
t.Fatalf("unexpected value: got %q, want %q", got, want)
}
// Use cmp.Diff for robust comparison
if diff := cmp.Diff(tc.want, gotAll); diff != "" {
t.Fatalf("ParseParams() mismatch (-want +got):\n%s", diff)
}
})
}
@@ -945,6 +1061,15 @@ func TestAuthParametersParse(t *testing.T) {
},
claimsMap: map[string]map[string]any{"my-google-auth-service": {"not_an_auth_field": "Alice"}},
},
{
name: "map",
params: tools.Parameters{
tools.NewMapParameterWithAuth("my_map", "a map", "string", authServices),
},
in: map[string]any{"my_map": map[string]any{"key1": "val1"}},
claimsMap: map[string]map[string]any{"my-google-auth-service": {"auth_field": map[string]any{"authed_key": "authed_val"}}},
want: tools.ParamValues{tools.ParamValue{Name: "my_map", Value: map[string]any{"authed_key": "authed_val"}}},
},
}
for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
@@ -970,15 +1095,9 @@ func TestAuthParametersParse(t *testing.T) {
}
t.Fatalf("unexpected error from ParseParams: %s", err)
}
for i, got := range gotAll {
want := tc.want[i]
if got != want {
t.Fatalf("unexpected value: got %q, want %q", got, want)
}
gotType, wantType := reflect.TypeOf(got), reflect.TypeOf(want)
if gotType != wantType {
t.Fatalf("unexpected value: got %q, want %q", got, want)
}
if diff := cmp.Diff(tc.want, gotAll); diff != "" {
t.Fatalf("ParseParams() mismatch (-want +got):\n%s", diff)
}
})
}
@@ -1142,12 +1261,48 @@ func TestParamManifest(t *testing.T) {
Items: &tools.ParameterManifest{Name: "foo-string", Type: "string", Required: false, Description: "bar", AuthServices: []string{}},
},
},
{
name: "map with string values",
in: tools.NewMapParameter("foo-map", "bar", "string"),
want: tools.ParameterManifest{
Name: "foo-map",
Type: "object",
Required: true,
Description: "bar",
AuthServices: []string{},
AdditionalProperties: &tools.ParameterManifest{Name: "", Type: "string", Required: true, Description: "", AuthServices: []string{}},
},
},
{
name: "map not required",
in: tools.NewMapParameterWithRequired("foo-map", "bar", false, "string"),
want: tools.ParameterManifest{
Name: "foo-map",
Type: "object",
Required: false,
Description: "bar",
AuthServices: []string{},
AdditionalProperties: &tools.ParameterManifest{Name: "", Type: "string", Required: true, Description: "", AuthServices: []string{}},
},
},
{
name: "generic map (additionalProperties true)",
in: tools.NewMapParameter("foo-map", "bar", ""),
want: tools.ParameterManifest{
Name: "foo-map",
Type: "object",
Required: true,
Description: "bar",
AuthServices: []string{},
AdditionalProperties: true,
},
},
}
for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
got := tc.in.Manifest()
if !reflect.DeepEqual(got, tc.want) {
t.Fatalf("unexpected manifest: got %+v, want %+v", got, tc.want)
if diff := cmp.Diff(tc.want, got); diff != "" {
t.Fatalf("unexpected manifest (-want +got):\n%s", diff)
}
})
}
@@ -1188,12 +1343,31 @@ func TestParamMcpManifest(t *testing.T) {
Items: &tools.ParameterMcpManifest{Type: "string", Description: "bar"},
},
},
{
name: "map with string values",
in: tools.NewMapParameter("foo-map", "bar", "string"),
want: tools.ParameterMcpManifest{
Type: "object",
Description: "bar",
AdditionalProperties: &tools.ParameterMcpManifest{Type: "string", Description: ""},
},
},
{
name: "generic map (additionalProperties true)",
in: tools.NewMapParameter("foo-map", "bar", ""),
want: tools.ParameterMcpManifest{
Type: "object",
Description: "bar",
AdditionalProperties: true,
},
},
}
for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
got := tc.in.McpManifest()
if !reflect.DeepEqual(got, tc.want) {
t.Fatalf("unexpected manifest: got %+v, want %+v", got, tc.want)
if diff := cmp.Diff(tc.want, got); diff != "" {
t.Fatalf("unexpected manifest (-want +got):\n%s", diff)
}
})
}
@@ -1206,46 +1380,46 @@ func TestMcpManifest(t *testing.T) {
want tools.McpToolsSchema
}{
{
name: "string",
name: "all types",
in: tools.Parameters{
tools.NewStringParameterWithDefault("foo-string", "foo", "bar"),
tools.NewStringParameter("foo-string2", "bar"),
tools.NewStringParameterWithRequired("foo-string-req", "bar", true),
tools.NewStringParameterWithRequired("foo-string-not-req", "bar", false),
tools.NewIntParameterWithDefault("foo-int", 1, "bar"),
tools.NewIntParameter("foo-int2", "bar"),
tools.NewArrayParameterWithDefault("foo-array", []any{"hello", "world"}, "bar", tools.NewStringParameter("foo-string", "bar")),
tools.NewArrayParameter("foo-array2", "bar", tools.NewStringParameter("foo-string", "bar")),
tools.NewMapParameter("foo-map-int", "a map of ints", "integer"),
tools.NewMapParameter("foo-map-any", "a map of any", ""),
},
want: tools.McpToolsSchema{
Type: "object",
Properties: map[string]tools.ParameterMcpManifest{
"foo-string": tools.ParameterMcpManifest{Type: "string", Description: "bar"},
"foo-string2": tools.ParameterMcpManifest{Type: "string", Description: "bar"},
"foo-string-req": tools.ParameterMcpManifest{Type: "string", Description: "bar"},
"foo-string-not-req": tools.ParameterMcpManifest{Type: "string", Description: "bar"},
"foo-int": tools.ParameterMcpManifest{Type: "integer", Description: "bar"},
"foo-int2": tools.ParameterMcpManifest{Type: "integer", Description: "bar"},
"foo-array": tools.ParameterMcpManifest{
"foo-string": {Type: "string", Description: "bar"},
"foo-string2": {Type: "string", Description: "bar"},
"foo-int2": {Type: "integer", Description: "bar"},
"foo-array2": {
Type: "array",
Description: "bar",
Items: &tools.ParameterMcpManifest{Type: "string", Description: "bar"},
},
"foo-array2": tools.ParameterMcpManifest{
Type: "array",
Description: "bar",
Items: &tools.ParameterMcpManifest{Type: "string", Description: "bar"},
"foo-map-int": {
Type: "object",
Description: "a map of ints",
AdditionalProperties: &tools.ParameterMcpManifest{Type: "integer", Description: ""},
},
"foo-map-any": {
Type: "object",
Description: "a map of any",
AdditionalProperties: true,
},
},
Required: []string{"foo-string2", "foo-string-req", "foo-int2", "foo-array2"},
Required: []string{"foo-string2", "foo-int2", "foo-array2", "foo-map-int", "foo-map-any"},
},
},
}
for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
got := tc.in.McpManifest()
if !reflect.DeepEqual(got, tc.want) {
t.Fatalf("unexpected manifest: got %+v, want %+v", got, tc.want)
if diff := cmp.Diff(tc.want, got); diff != "" {
t.Fatalf("unexpected manifest (-want +got):\n%s", diff)
}
})
}
@@ -1317,6 +1491,19 @@ func TestFailParametersUnmarshal(t *testing.T) {
},
err: "unable to parse as \"array\": unable to parse 'items' field: unable to parse as \"string\": Key: 'CommonParameter.Name' Error:Field validation for 'Name' failed on the 'required' tag",
},
// --- MODIFIED MAP PARAMETER TEST ---
{
name: "map with invalid valueType",
in: []map[string]any{
{
"name": "my_map",
"type": "map",
"description": "this param is a map",
"valueType": "not-a-real-type",
},
},
err: "unsupported valueType \"not-a-real-type\" for map parameter",
},
}
for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
@@ -1332,14 +1519,18 @@ func TestFailParametersUnmarshal(t *testing.T) {
t.Fatalf("expect parsing to fail")
}
errStr := err.Error()
if errStr != tc.err {
t.Fatalf("unexpected error: got %q, want %q", errStr, tc.err)
if !strings.Contains(errStr, tc.err) {
t.Fatalf("unexpected error: got %q, want to contain %q", errStr, tc.err)
}
})
}
}
// ... (Remaining test functions do not involve parameter definitions and need no changes)
func TestConvertArrayParamToString(t *testing.T) {
tcs := []struct {
name string
in []any
@@ -1482,6 +1673,7 @@ func TestGetParams(t *testing.T) {
}
func TestFailGetParams(t *testing.T) {
tcs := []struct {
name string
params tools.Parameters

View File

@@ -19,6 +19,7 @@ import (
"encoding/json"
"fmt"
"io"
"strings"
"github.com/go-playground/validator/v10"
yaml "github.com/goccy/go-yaml"
@@ -36,6 +37,46 @@ func DecodeJSON(r io.Reader, v interface{}) error {
return d.Decode(v)
}
// ConvertNumbers traverses an interface and converts all json.Number
// instances to int64 or float64.
func ConvertNumbers(data any) (any, error) {
switch v := data.(type) {
// If it's a map, recursively convert the values.
case map[string]any:
for key, val := range v {
convertedVal, err := ConvertNumbers(val)
if err != nil {
return nil, err
}
v[key] = convertedVal
}
return v, nil
// If it's a slice, recursively convert the elements.
case []any:
for i, val := range v {
convertedVal, err := ConvertNumbers(val)
if err != nil {
return nil, err
}
v[i] = convertedVal
}
return v, nil
// If it's a json.Number, convert it to float or int
case json.Number:
// Check for a decimal point to decide the type.
if strings.Contains(v.String(), ".") {
return v.Float64()
}
return v.Int64()
// For all other types, return them as is.
default:
return data, nil
}
}
var _ yaml.InterfaceUnmarshalerContext = &DelayedUnmarshaler{}
// DelayedUnmarshaler is struct that saves the provided unmarshal function