Files

193 lines
5.5 KiB
Go

package basic
import (
"encoding/json"
"errors"
"fmt"
"math"
)
// Sentinel errors returned by the basic tools. Use errors.Is to test for them.
var (
// ErrInvalidArgument is returned when a tool is given a missing required
// argument or an argument of the wrong type.
ErrInvalidArgument = errors.New("basic: invalid argument")
// ErrUnsupported is returned when an operation is not in the tool allowlist.
ErrUnsupported = errors.New("basic: unsupported operation")
// ErrEvaluation is returned when a calculator expression cannot be parsed or
// evaluated.
ErrEvaluation = errors.New("basic: evaluation failed")
)
// asMap asserts args is a nil value or a map[string]any — the shape the
// dispatcher passes when a tool's Parameters declares an object schema.
func asMap(args any) (map[string]any, error) {
if args == nil {
return map[string]any{}, nil
}
m, ok := args.(map[string]any)
if !ok {
return nil, fmt.Errorf("basic: arguments must be an object: %w", ErrInvalidArgument)
}
return m, nil
}
// hasKey reports whether m contains key.
func hasKey(m map[string]any, key string) bool {
_, ok := m[key]
return ok
}
// reqString returns the string argument under key, erroring when it is absent or
// not a string.
func reqString(m map[string]any, key string) (string, error) {
v, ok := m[key]
if !ok {
return "", fmt.Errorf("basic: missing required argument %q: %w", key, ErrInvalidArgument)
}
s, ok := v.(string)
if !ok {
return "", fmt.Errorf("basic: argument %q must be a string: %w", key, ErrInvalidArgument)
}
return s, nil
}
// optString returns the string argument under key, or def when absent. A present
// non-string is an error.
func optString(m map[string]any, key, def string) (string, error) {
if !hasKey(m, key) {
return def, nil
}
return reqString(m, key)
}
// reqInt returns the integer argument under key, erroring when it is absent, not
// a number, or not integral.
func reqInt(m map[string]any, key string) (int, error) {
v, ok := m[key]
if !ok {
return 0, fmt.Errorf("basic: missing required argument %q: %w", key, ErrInvalidArgument)
}
n, err := asInt(v)
if err != nil {
return 0, fmt.Errorf("basic: argument %q: %w", key, err)
}
return n, nil
}
// optInt returns the integer argument under key, or def when absent.
func optInt(m map[string]any, key string, def int) (int, error) {
if !hasKey(m, key) {
return def, nil
}
return reqInt(m, key)
}
// optBool returns the boolean argument under key, or def when absent.
func optBool(m map[string]any, key string, def bool) (bool, error) {
if !hasKey(m, key) {
return def, nil
}
v, ok := m[key].(bool)
if !ok {
return false, fmt.Errorf("basic: argument %q must be a boolean: %w", key, ErrInvalidArgument)
}
return v, nil
}
// reqStringMap returns the object argument under key coerced to map[string]string,
// erroring when a value is not a string.
func reqStringMap(m map[string]any, key string) (map[string]string, error) {
v, ok := m[key]
if !ok {
return nil, fmt.Errorf("basic: missing required argument %q: %w", key, ErrInvalidArgument)
}
raw, ok := v.(map[string]any)
if !ok {
return nil, fmt.Errorf("basic: argument %q must be an object: %w", key, ErrInvalidArgument)
}
out := make(map[string]string, len(raw))
for k, val := range raw {
s, ok := val.(string)
if !ok {
return nil, fmt.Errorf("basic: value %q must be a string: %w", k, ErrInvalidArgument)
}
out[k] = s
}
return out, nil
}
// strSliceArg returns the array-of-strings argument under key.
func strSliceArg(m map[string]any, key string) ([]string, error) {
v, ok := m[key]
if !ok {
return nil, fmt.Errorf("basic: missing required argument %q: %w", key, ErrInvalidArgument)
}
raw, ok := v.([]any)
if !ok {
return nil, fmt.Errorf("basic: argument %q must be an array: %w", key, ErrInvalidArgument)
}
out := make([]string, len(raw))
for i, item := range raw {
s, ok := item.(string)
if !ok {
return nil, fmt.Errorf("basic: argument %q item %d must be a string: %w", key, i, ErrInvalidArgument)
}
out[i] = s
}
return out, nil
}
// strSliceSliceArg returns the array-of-arrays-of-strings argument under key.
func strSliceSliceArg(m map[string]any, key string) ([][]string, error) {
v, ok := m[key]
if !ok {
return nil, fmt.Errorf("basic: missing required argument %q: %w", key, ErrInvalidArgument)
}
raw, ok := v.([]any)
if !ok {
return nil, fmt.Errorf("basic: argument %q must be an array: %w", key, ErrInvalidArgument)
}
out := make([][]string, len(raw))
for i, item := range raw {
row, ok := item.([]any)
if !ok {
return nil, fmt.Errorf("basic: argument %q item %d must be an array: %w", key, i, ErrInvalidArgument)
}
cells := make([]string, len(row))
for j, c := range row {
s, ok := c.(string)
if !ok {
return nil, fmt.Errorf("basic: argument %q item %d cell %d must be a string: %w", key, i, j, ErrInvalidArgument)
}
cells[j] = s
}
out[i] = cells
}
return out, nil
}
// asInt converts a numeric-typed value (the types the dispatcher decodes JSON
// into) to an int, rejecting non-integral floats.
func asInt(v any) (int, error) {
switch x := v.(type) {
case int:
return x, nil
case int64:
return int(x), nil
case float64:
if x != math.Trunc(x) || x < math.MinInt64 || x > math.MaxInt64 {
return 0, fmt.Errorf("must be an integer: %w", ErrInvalidArgument)
}
return int(x), nil
case json.Number:
n, err := x.Int64()
if err != nil {
return 0, fmt.Errorf("must be an integer: %w", ErrInvalidArgument)
}
return int(n), nil
default:
return 0, fmt.Errorf("must be a number: %w", ErrInvalidArgument)
}
}