mirror of
https://github.com/googleapis/genai-toolbox.git
synced 2026-01-09 15:38:08 -05:00
feat: add support for null optional parameter (#802)
Added an option for user to indicate if the parameter is required.
Example:
```
parameters:
- name: foo
type: string
description: foo
required: false
```
If the `required` field is not provided, it will be defaulted to `true`.
If a `default` value is provided, `required = false` regardless if the
field is indicated.
```
parameters:
- name: foo
type: string
description: foo
default: hello world
```
Fixes #736
This commit is contained in:
@@ -109,11 +109,17 @@ func parseFromAuthService(paramAuthServices []ParamAuthService, claimsMap map[st
|
||||
return nil, fmt.Errorf("missing or invalid authentication header")
|
||||
}
|
||||
|
||||
// CheckParamRequired checks if a parameter is required based on the required and default field.
|
||||
func CheckParamRequired(required bool, defaultV any) bool {
|
||||
return required && defaultV == nil
|
||||
}
|
||||
|
||||
// ParseParams is a helper function for parsing Parameters from an arbitraryJSON object.
|
||||
func ParseParams(ps Parameters, data map[string]any, claimsMap map[string]map[string]any) (ParamValues, error) {
|
||||
params := make([]ParamValue, 0, len(ps))
|
||||
for _, p := range ps {
|
||||
var v any
|
||||
var v, newV any
|
||||
var err error
|
||||
paramAuthServices := p.GetAuthServices()
|
||||
name := p.GetName()
|
||||
if len(paramAuthServices) == 0 {
|
||||
@@ -122,21 +128,23 @@ func ParseParams(ps Parameters, data map[string]any, claimsMap map[string]map[st
|
||||
v, ok = data[name]
|
||||
if !ok {
|
||||
v = p.GetDefault()
|
||||
if v == nil {
|
||||
// if the parameter is required and no value given, throw an error
|
||||
if CheckParamRequired(p.GetRequired(), v) {
|
||||
return nil, fmt.Errorf("parameter %q is required", name)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// parse authenticated parameter
|
||||
var err error
|
||||
v, err = parseFromAuthService(paramAuthServices, claimsMap)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing authenticated parameter %q: %w", name, err)
|
||||
}
|
||||
}
|
||||
newV, err := p.Parse(v)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to parse value for %q: %w", name, err)
|
||||
if v != nil {
|
||||
newV, err = p.Parse(v)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to parse value for %q: %w", name, err)
|
||||
}
|
||||
}
|
||||
params = append(params, ParamValue{Name: name, Value: newV})
|
||||
}
|
||||
@@ -248,6 +256,7 @@ type Parameter interface {
|
||||
GetName() string
|
||||
GetType() string
|
||||
GetDefault() any
|
||||
GetRequired() bool
|
||||
GetAuthServices() []ParamAuthService
|
||||
Parse(any) (any, error)
|
||||
Manifest() ParameterManifest
|
||||
@@ -378,7 +387,7 @@ func (ps Parameters) McpManifest() McpToolsSchema {
|
||||
name := p.GetName()
|
||||
properties[name] = p.McpManifest()
|
||||
// parameters that doesn't have a default value are added to the required field
|
||||
if p.GetDefault() == nil {
|
||||
if CheckParamRequired(p.GetRequired(), p.GetDefault()) {
|
||||
required = append(required, name)
|
||||
}
|
||||
}
|
||||
@@ -412,6 +421,7 @@ type CommonParameter struct {
|
||||
Name string `yaml:"name" validate:"required"`
|
||||
Type string `yaml:"type" validate:"required"`
|
||||
Desc string `yaml:"description" validate:"required"`
|
||||
Required *bool `yaml:"required"`
|
||||
AuthServices []ParamAuthService `yaml:"authServices"`
|
||||
AuthSources []ParamAuthService `yaml:"authSources"` // Deprecated: Kept for compatibility.
|
||||
}
|
||||
@@ -426,6 +436,15 @@ func (p *CommonParameter) GetType() string {
|
||||
return p.Type
|
||||
}
|
||||
|
||||
// GetRequired returns the type specified for the Parameter.
|
||||
func (p *CommonParameter) GetRequired() bool {
|
||||
// parameters are defaulted to required
|
||||
if p.Required == nil {
|
||||
return true
|
||||
}
|
||||
return *p.Required
|
||||
}
|
||||
|
||||
// McpManifest returns the MCP manifest for the Parameter.
|
||||
func (p *CommonParameter) McpManifest() ParameterMcpManifest {
|
||||
return ParameterMcpManifest{
|
||||
@@ -475,6 +494,19 @@ func NewStringParameterWithDefault(name string, defaultV, desc string) *StringPa
|
||||
}
|
||||
}
|
||||
|
||||
// NewStringParameterWithRequired is a convenience function for initializing a StringParameter.
|
||||
func NewStringParameterWithRequired(name string, desc string, required bool) *StringParameter {
|
||||
return &StringParameter{
|
||||
CommonParameter: CommonParameter{
|
||||
Name: name,
|
||||
Type: typeString,
|
||||
Desc: desc,
|
||||
Required: &required,
|
||||
AuthServices: nil,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NewStringParameterWithAuth is a convenience function for initializing a StringParameter with a list of ParamAuthService.
|
||||
func NewStringParameterWithAuth(name string, desc string, authServices []ParamAuthService) *StringParameter {
|
||||
return &StringParameter{
|
||||
@@ -522,11 +554,11 @@ func (p *StringParameter) Manifest() ParameterManifest {
|
||||
for i, a := range p.AuthServices {
|
||||
authNames[i] = a.Name
|
||||
}
|
||||
required := p.Default == nil
|
||||
r := CheckParamRequired(p.GetRequired(), p.GetDefault())
|
||||
return ParameterManifest{
|
||||
Name: p.Name,
|
||||
Type: p.Type,
|
||||
Required: required,
|
||||
Required: r,
|
||||
Description: p.Desc,
|
||||
AuthServices: authNames,
|
||||
}
|
||||
@@ -557,6 +589,19 @@ func NewIntParameterWithDefault(name string, defaultV int, desc string) *IntPara
|
||||
}
|
||||
}
|
||||
|
||||
// NewIntParameterWithRequired is a convenience function for initializing a IntParameter.
|
||||
func NewIntParameterWithRequired(name string, desc string, required bool) *IntParameter {
|
||||
return &IntParameter{
|
||||
CommonParameter: CommonParameter{
|
||||
Name: name,
|
||||
Type: typeInt,
|
||||
Desc: desc,
|
||||
Required: &required,
|
||||
AuthServices: nil,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NewIntParameterWithAuth is a convenience function for initializing a IntParameter with a list of ParamAuthService.
|
||||
func NewIntParameterWithAuth(name string, desc string, authServices []ParamAuthService) *IntParameter {
|
||||
return &IntParameter{
|
||||
@@ -616,11 +661,11 @@ func (p *IntParameter) Manifest() ParameterManifest {
|
||||
for i, a := range p.AuthServices {
|
||||
authNames[i] = a.Name
|
||||
}
|
||||
required := p.Default == nil
|
||||
r := CheckParamRequired(p.GetRequired(), p.GetDefault())
|
||||
return ParameterManifest{
|
||||
Name: p.Name,
|
||||
Type: p.Type,
|
||||
Required: required,
|
||||
Required: r,
|
||||
Description: p.Desc,
|
||||
AuthServices: authNames,
|
||||
}
|
||||
@@ -651,6 +696,19 @@ func NewFloatParameterWithDefault(name string, defaultV float64, desc string) *F
|
||||
}
|
||||
}
|
||||
|
||||
// NewFloatParameterWithRequired is a convenience function for initializing a FloatParameter.
|
||||
func NewFloatParameterWithRequired(name string, desc string, required bool) *FloatParameter {
|
||||
return &FloatParameter{
|
||||
CommonParameter: CommonParameter{
|
||||
Name: name,
|
||||
Type: typeFloat,
|
||||
Desc: desc,
|
||||
Required: &required,
|
||||
AuthServices: nil,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NewFloatParameterWithAuth is a convenience function for initializing a FloatParameter with a list of ParamAuthService.
|
||||
func NewFloatParameterWithAuth(name string, desc string, authServices []ParamAuthService) *FloatParameter {
|
||||
return &FloatParameter{
|
||||
@@ -708,11 +766,11 @@ func (p *FloatParameter) Manifest() ParameterManifest {
|
||||
for i, a := range p.AuthServices {
|
||||
authNames[i] = a.Name
|
||||
}
|
||||
required := p.Default == nil
|
||||
r := CheckParamRequired(p.GetRequired(), p.GetDefault())
|
||||
return ParameterManifest{
|
||||
Name: p.Name,
|
||||
Type: p.Type,
|
||||
Required: required,
|
||||
Required: r,
|
||||
Description: p.Desc,
|
||||
AuthServices: authNames,
|
||||
}
|
||||
@@ -743,6 +801,19 @@ func NewBooleanParameterWithDefault(name string, defaultV bool, desc string) *Bo
|
||||
}
|
||||
}
|
||||
|
||||
// NewBooleanParameterWithRequired is a convenience function for initializing a BooleanParameter.
|
||||
func NewBooleanParameterWithRequired(name string, desc string, required bool) *BooleanParameter {
|
||||
return &BooleanParameter{
|
||||
CommonParameter: CommonParameter{
|
||||
Name: name,
|
||||
Type: typeBool,
|
||||
Desc: desc,
|
||||
Required: &required,
|
||||
AuthServices: nil,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NewBooleanParameterWithAuth is a convenience function for initializing a BooleanParameter with a list of ParamAuthService.
|
||||
func NewBooleanParameterWithAuth(name string, desc string, authServices []ParamAuthService) *BooleanParameter {
|
||||
return &BooleanParameter{
|
||||
@@ -789,11 +860,11 @@ func (p *BooleanParameter) Manifest() ParameterManifest {
|
||||
for i, a := range p.AuthServices {
|
||||
authNames[i] = a.Name
|
||||
}
|
||||
required := p.Default == nil
|
||||
r := CheckParamRequired(p.GetRequired(), p.GetDefault())
|
||||
return ParameterManifest{
|
||||
Name: p.Name,
|
||||
Type: p.Type,
|
||||
Required: required,
|
||||
Required: r,
|
||||
Description: p.Desc,
|
||||
AuthServices: authNames,
|
||||
}
|
||||
@@ -826,6 +897,20 @@ func NewArrayParameterWithDefault(name string, defaultV []any, desc string, item
|
||||
}
|
||||
}
|
||||
|
||||
// NewArrayParameterWithRequired is a convenience function for initializing a ArrayParameter with default value.
|
||||
func NewArrayParameterWithRequired(name string, desc string, required bool, items Parameter) *ArrayParameter {
|
||||
return &ArrayParameter{
|
||||
CommonParameter: CommonParameter{
|
||||
Name: name,
|
||||
Type: typeArray,
|
||||
Desc: desc,
|
||||
Required: &required,
|
||||
AuthServices: nil,
|
||||
},
|
||||
Items: items,
|
||||
}
|
||||
}
|
||||
|
||||
// NewArrayParameterWithAuth is a convenience function for initializing a ArrayParameter with a list of ParamAuthService.
|
||||
func NewArrayParameterWithAuth(name string, desc string, items Parameter, authServices []ParamAuthService) *ArrayParameter {
|
||||
return &ArrayParameter{
|
||||
@@ -910,12 +995,13 @@ func (p *ArrayParameter) Manifest() ParameterManifest {
|
||||
authNames[i] = a.Name
|
||||
}
|
||||
items := p.Items.Manifest()
|
||||
required := p.Default == nil
|
||||
items.Required = required
|
||||
// if required value is true, or there's no default value
|
||||
r := CheckParamRequired(p.GetRequired(), p.GetDefault())
|
||||
items.Required = r
|
||||
return ParameterManifest{
|
||||
Name: p.Name,
|
||||
Type: p.Type,
|
||||
Required: required,
|
||||
Required: r,
|
||||
Description: p.Desc,
|
||||
AuthServices: authNames,
|
||||
Items: &items,
|
||||
|
||||
@@ -50,6 +50,20 @@ func TestParametersMarshal(t *testing.T) {
|
||||
tools.NewStringParameter("my_string", "this param is a string"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "string not required",
|
||||
in: []map[string]any{
|
||||
{
|
||||
"name": "my_string",
|
||||
"type": "string",
|
||||
"description": "this param is a string",
|
||||
"required": false,
|
||||
},
|
||||
},
|
||||
want: tools.Parameters{
|
||||
tools.NewStringParameterWithRequired("my_string", "this param is a string", false),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "int",
|
||||
in: []map[string]any{
|
||||
@@ -63,6 +77,20 @@ func TestParametersMarshal(t *testing.T) {
|
||||
tools.NewIntParameter("my_integer", "this param is an int"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "int not required",
|
||||
in: []map[string]any{
|
||||
{
|
||||
"name": "my_integer",
|
||||
"type": "integer",
|
||||
"description": "this param is an int",
|
||||
"required": false,
|
||||
},
|
||||
},
|
||||
want: tools.Parameters{
|
||||
tools.NewIntParameterWithRequired("my_integer", "this param is an int", false),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "float",
|
||||
in: []map[string]any{
|
||||
@@ -76,6 +104,20 @@ func TestParametersMarshal(t *testing.T) {
|
||||
tools.NewFloatParameter("my_float", "my param is a float"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "float not required",
|
||||
in: []map[string]any{
|
||||
{
|
||||
"name": "my_float",
|
||||
"type": "float",
|
||||
"description": "my param is a float",
|
||||
"required": false,
|
||||
},
|
||||
},
|
||||
want: tools.Parameters{
|
||||
tools.NewFloatParameterWithRequired("my_float", "my param is a float", false),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "bool",
|
||||
in: []map[string]any{
|
||||
@@ -89,6 +131,20 @@ func TestParametersMarshal(t *testing.T) {
|
||||
tools.NewBooleanParameter("my_bool", "this param is a boolean"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "bool not required",
|
||||
in: []map[string]any{
|
||||
{
|
||||
"name": "my_bool",
|
||||
"type": "boolean",
|
||||
"description": "this param is a boolean",
|
||||
"required": false,
|
||||
},
|
||||
},
|
||||
want: tools.Parameters{
|
||||
tools.NewBooleanParameterWithRequired("my_bool", "this param is a boolean", false),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "string array",
|
||||
in: []map[string]any{
|
||||
@@ -107,6 +163,25 @@ func TestParametersMarshal(t *testing.T) {
|
||||
tools.NewArrayParameter("my_array", "this param is an array of strings", tools.NewStringParameter("my_string", "string item")),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "string array not required",
|
||||
in: []map[string]any{
|
||||
{
|
||||
"name": "my_array",
|
||||
"type": "array",
|
||||
"description": "this param is an array of strings",
|
||||
"required": false,
|
||||
"items": map[string]string{
|
||||
"name": "my_string",
|
||||
"type": "string",
|
||||
"description": "string item",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: tools.Parameters{
|
||||
tools.NewArrayParameterWithRequired("my_array", "this param is an array of strings", false, tools.NewStringParameter("my_string", "string item")),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "float array",
|
||||
in: []map[string]any{
|
||||
@@ -673,6 +748,38 @@ func TestParametersParse(t *testing.T) {
|
||||
in: map[string]any{},
|
||||
want: tools.ParamValues{tools.ParamValue{Name: "my_bool", Value: true}},
|
||||
},
|
||||
{
|
||||
name: "string not required",
|
||||
params: tools.Parameters{
|
||||
tools.NewStringParameterWithRequired("my_string", "this param is a string", false),
|
||||
},
|
||||
in: map[string]any{},
|
||||
want: tools.ParamValues{tools.ParamValue{Name: "my_string", Value: nil}},
|
||||
},
|
||||
{
|
||||
name: "int not required",
|
||||
params: tools.Parameters{
|
||||
tools.NewIntParameterWithRequired("my_int", "this param is an int", false),
|
||||
},
|
||||
in: map[string]any{},
|
||||
want: tools.ParamValues{tools.ParamValue{Name: "my_int", Value: nil}},
|
||||
},
|
||||
{
|
||||
name: "float not required",
|
||||
params: tools.Parameters{
|
||||
tools.NewFloatParameterWithRequired("my_float", "this param is a float", false),
|
||||
},
|
||||
in: map[string]any{},
|
||||
want: tools.ParamValues{tools.ParamValue{Name: "my_float", Value: nil}},
|
||||
},
|
||||
{
|
||||
name: "bool not required",
|
||||
params: tools.Parameters{
|
||||
tools.NewBooleanParameterWithRequired("my_bool", "this param is a bool", false),
|
||||
},
|
||||
in: map[string]any{},
|
||||
want: tools.ParamValues{tools.ParamValue{Name: "my_bool", Value: nil}},
|
||||
},
|
||||
}
|
||||
for _, tc := range tcs {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
@@ -1003,6 +1110,38 @@ func TestParamManifest(t *testing.T) {
|
||||
Items: &tools.ParameterManifest{Name: "foo-string", Type: "string", Required: false, Description: "bar", AuthServices: []string{}},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "string not required",
|
||||
in: tools.NewStringParameterWithRequired("foo-string", "bar", false),
|
||||
want: tools.ParameterManifest{Name: "foo-string", Type: "string", Required: false, Description: "bar", AuthServices: []string{}},
|
||||
},
|
||||
{
|
||||
name: "int not required",
|
||||
in: tools.NewIntParameterWithRequired("foo-int", "bar", false),
|
||||
want: tools.ParameterManifest{Name: "foo-int", Type: "integer", Required: false, Description: "bar", AuthServices: []string{}},
|
||||
},
|
||||
{
|
||||
name: "float not required",
|
||||
in: tools.NewFloatParameterWithRequired("foo-float", "bar", false),
|
||||
want: tools.ParameterManifest{Name: "foo-float", Type: "float", Required: false, Description: "bar", AuthServices: []string{}},
|
||||
},
|
||||
{
|
||||
name: "boolean not required",
|
||||
in: tools.NewBooleanParameterWithRequired("foo-bool", "bar", false),
|
||||
want: tools.ParameterManifest{Name: "foo-bool", Type: "boolean", Required: false, Description: "bar", AuthServices: []string{}},
|
||||
},
|
||||
{
|
||||
name: "array not required",
|
||||
in: tools.NewArrayParameterWithRequired("foo-array", "bar", false, tools.NewStringParameter("foo-string", "bar")),
|
||||
want: tools.ParameterManifest{
|
||||
Name: "foo-array",
|
||||
Type: "array",
|
||||
Required: false,
|
||||
Description: "bar",
|
||||
AuthServices: []string{},
|
||||
Items: &tools.ParameterManifest{Name: "foo-string", Type: "string", Required: false, Description: "bar", AuthServices: []string{}},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tc := range tcs {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
@@ -1071,6 +1210,8 @@ func TestMcpManifest(t *testing.T) {
|
||||
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")),
|
||||
@@ -1079,10 +1220,12 @@ func TestMcpManifest(t *testing.T) {
|
||||
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-int": tools.ParameterMcpManifest{Type: "integer", Description: "bar"},
|
||||
"foo-int2": tools.ParameterMcpManifest{Type: "integer", Description: "bar"},
|
||||
"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{
|
||||
Type: "array",
|
||||
Description: "bar",
|
||||
@@ -1094,7 +1237,7 @@ func TestMcpManifest(t *testing.T) {
|
||||
Items: &tools.ParameterMcpManifest{Type: "string", Description: "bar"},
|
||||
},
|
||||
},
|
||||
Required: []string{"foo-string2", "foo-int2", "foo-array2"},
|
||||
Required: []string{"foo-string2", "foo-string-req", "foo-int2", "foo-array2"},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1502,3 +1645,45 @@ func TestFailResolveTemplateParameters(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckParamRequired(t *testing.T) {
|
||||
tcs := []struct {
|
||||
name string
|
||||
required bool
|
||||
defaultV any
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "required and no default",
|
||||
required: true,
|
||||
defaultV: nil,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "required and default",
|
||||
required: true,
|
||||
defaultV: "foo",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "not required and no default",
|
||||
required: false,
|
||||
defaultV: nil,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "not required and default",
|
||||
required: false,
|
||||
defaultV: "foo",
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
for _, tc := range tcs {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got := tools.CheckParamRequired(tc.required, tc.defaultV)
|
||||
if got != tc.want {
|
||||
t.Fatalf("got %v, want %v", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user