diff --git a/cmdlang/eval.go b/cmdlang/eval.go index 38eb13b..828b526 100644 --- a/cmdlang/eval.go +++ b/cmdlang/eval.go @@ -4,7 +4,6 @@ import ( "context" "errors" "fmt" - "github.com/lmika/gopkgs/fp/slices" "strconv" "strings" ) @@ -86,14 +85,32 @@ func (e evaluator) evalCmd(ctx context.Context, ec *evalCtx, currentStream strea } func (e evaluator) evalInvokable(ctx context.Context, ec *evalCtx, currentStream stream, ast *astCmd, cmd invokable) (object, error) { - args, err := slices.MapWithError(ast.Args, func(a astCmdArg) (object, error) { - return e.evalArg(ctx, ec, a) - }) - if err != nil { - return nil, err + var ( + pargs listObject + kwargs map[string]*listObject + argsPtr *listObject + ) + + argsPtr = &pargs + for _, arg := range ast.Args { + if ident := arg.Ident; ident != nil && (*ident)[0] == '-' { + // Arg switch + if kwargs == nil { + kwargs = make(map[string]*listObject) + } + + argsPtr = &listObject{} + kwargs[(*ident)[1:]] = argsPtr + } else { + ae, err := e.evalArg(ctx, ec, arg) + if err != nil { + return nil, err + } + argsPtr.Append(ae) + } } - invArgs := invocationArgs{ec: ec, inst: e.inst, args: args, currentStream: currentStream} + invArgs := invocationArgs{ec: ec, inst: e.inst, args: pargs, kwargs: kwargs, currentStream: currentStream} if currentStream != nil { if si, ok := cmd.(streamInvokable); ok { diff --git a/cmdlang/objs.go b/cmdlang/objs.go index b08ed21..f71074d 100644 --- a/cmdlang/objs.go +++ b/cmdlang/objs.go @@ -14,6 +14,10 @@ type object interface { type listObject []object +func (lo *listObject) Append(o object) { + *lo = append(*lo, o) +} + func (s listObject) String() string { return fmt.Sprintf("%v", []object(s)) } @@ -152,6 +156,7 @@ type invocationArgs struct { ec *evalCtx currentStream stream args []object + kwargs map[string]*listObject } func (ia invocationArgs) expectArgn(x int) error { diff --git a/cmdlang/userbuiltin.go b/cmdlang/userbuiltin.go index ec19b0d..7e825f3 100644 --- a/cmdlang/userbuiltin.go +++ b/cmdlang/userbuiltin.go @@ -16,32 +16,35 @@ func (ca CallArgs) Bind(vars ...interface{}) error { } for i, v := range vars { - switch t := v.(type) { - case *string: - tv, err := ca.args.stringArg(i) - if err != nil { - return err - } - *t = tv - } - - // Check for proxy object - if po, ok := ca.args.args[i].(proxyObject); ok { - poValue := reflect.ValueOf(po.p) - argValue := reflect.ValueOf(v) - - if argValue.Type().Kind() != reflect.Pointer { - continue - } else if !poValue.Type().AssignableTo(argValue.Elem().Type()) { - continue - } - - argValue.Elem().Set(poValue) + if err := bindArg(v, ca.args.args[i]); err != nil { + return err } } return nil } +func (ca CallArgs) HasSwitch(name string) bool { + if ca.args.kwargs == nil { + return false + } + + _, ok := ca.args.kwargs[name] + return ok +} + +func (ca CallArgs) BindSwitch(name string, val interface{}) error { + if ca.args.kwargs == nil { + return nil + } + + vars, ok := ca.args.kwargs[name] + if !ok || len(*vars) != 1 { + return nil + } + + return bindArg(val, (*vars)[0]) +} + func (inst *Inst) SetBuiltin(name string, fn func(ctx context.Context, args CallArgs) (any, error)) { inst.rootEC.addCmd(name, userBuiltin{fn: fn}) } @@ -58,3 +61,25 @@ func (u userBuiltin) invoke(ctx context.Context, args invocationArgs) (object, e return fromGoValue(v) } + +func bindArg(v interface{}, arg object) error { + switch t := v.(type) { + case *string: + *t = arg.String() + } + + // Check for proxy object + if po, ok := arg.(proxyObject); ok { + poValue := reflect.ValueOf(po.p) + argValue := reflect.ValueOf(v) + + if argValue.Type().Kind() != reflect.Pointer { + return nil + } else if !poValue.Type().AssignableTo(argValue.Elem().Type()) { + return nil + } + + argValue.Elem().Set(poValue) + } + return nil +} diff --git a/cmdlang/userbuiltin_test.go b/cmdlang/userbuiltin_test.go index 5f42016..5c93e1a 100644 --- a/cmdlang/userbuiltin_test.go +++ b/cmdlang/userbuiltin_test.go @@ -2,6 +2,7 @@ package cmdlang_test import ( "context" + "strings" "testing" "github.com/lmika/cmdlang-proto/cmdlang" @@ -26,6 +27,48 @@ func TestInst_SetBuiltin(t *testing.T) { assert.Equal(t, "Hello, World", res) }) + t.Run("simple builtin with optional switches and strings", func(t *testing.T) { + inst := cmdlang.New() + inst.SetBuiltin("add2", func(ctx context.Context, args cmdlang.CallArgs) (any, error) { + var x, y, sep string + + if err := args.BindSwitch("sep", &sep); err != nil { + return nil, err + } + if err := args.BindSwitch("left", &x); err != nil { + return nil, err + } + if err := args.BindSwitch("right", &y); err != nil { + return nil, err + } + + v := x + sep + y + if args.HasSwitch("upcase") { + v = strings.ToUpper(v) + } + + return v, nil + }) + + tests := []struct { + descr string + expr string + want string + }{ + {"plain eval", `add2 -sep ", " -right "world" -left "Hello"`, "Hello, world"}, + {"swap 1", `add2 -right "right" -left "left" -sep ":"`, "left:right"}, + {"swap 2", `add2 -left "left" -sep ":" -right "right" -upcase`, "LEFT:RIGHT"}, + } + + for _, tt := range tests { + t.Run(tt.descr, func(t *testing.T) { + res, err := inst.Eval(context.Background(), tt.expr) + assert.NoError(t, err) + assert.Equal(t, tt.want, res) + }) + } + }) + t.Run("builtin return proxy object", func(t *testing.T) { type pair struct { x, y string