mirror of
https://github.com/googleapis/genai-toolbox.git
synced 2026-01-12 08:58:28 -05:00
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:
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user