From 219d4d49dad13c584cd061554fe8c1893b3fc1d6 Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Sun, 14 Apr 2024 22:09:13 +1000 Subject: [PATCH] Started working on the public interface --- cmdlang/inst_test.go | 117 ---------------------------------- cmdlang/objs.go | 26 ++++++++ cmdlang/testbuiltins_test.go | 120 +++++++++++++++++++++++++++++++++++ cmdlang/userbuiltin.go | 60 ++++++++++++++++++ cmdlang/userbuiltin_test.go | 91 ++++++++++++++++++++++++++ 5 files changed, 297 insertions(+), 117 deletions(-) create mode 100644 cmdlang/userbuiltin.go create mode 100644 cmdlang/userbuiltin_test.go diff --git a/cmdlang/inst_test.go b/cmdlang/inst_test.go index 96455df..2ca544a 100644 --- a/cmdlang/inst_test.go +++ b/cmdlang/inst_test.go @@ -52,120 +52,3 @@ func TestInst_Eval(t *testing.T) { }) } } - -func TestBuiltins_Echo(t *testing.T) { - tests := []struct { - desc string - expr string - want string - }{ - {desc: "no args", expr: `echo`, want: "\n"}, - {desc: "single arg", expr: `echo "hello"`, want: "hello\n"}, - {desc: "dual args", expr: `echo "hello " "world"`, want: "hello world\n"}, - {desc: "multi-line 1", expr: ` - echo "Hello" - echo "world" - `, want: "Hello\nworld\n"}, - {desc: "multi-line 2", expr: ` - echo "Hello" - - - echo "world" - `, want: "Hello\nworld\n"}, - {desc: "multi-line 3", expr: ` - -;;; - echo "Hello" -; - - echo "world" -; - `, want: "Hello\nworld\n"}, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - ctx := context.Background() - outW := bytes.NewBuffer(nil) - - inst := cmdlang.New(cmdlang.WithOut(outW), cmdlang.WithTestBuiltin()) - res, err := inst.Eval(ctx, tt.expr) - - assert.NoError(t, err) - assert.Nil(t, res) - assert.Equal(t, tt.want, outW.String()) - }) - } -} - -func TestBuiltins_If(t *testing.T) { - tests := []struct { - desc string - expr string - want string - }{ - {desc: "single then", expr: ` - set x "Hello" - if $x { - echo "true" - }`, want: "true\n(nil)\n"}, - {desc: "single then and else", expr: ` - set x "Hello" - if $x { - echo "true" - } else { - echo "false" - }`, want: "true\n(nil)\n"}, - {desc: "single then, elif and else", expr: ` - set x "Hello" - if $y { - echo "y is true" - } elif $x { - echo "x is true" - } else { - echo "nothings x" - }`, want: "x is true\n(nil)\n"}, - {desc: "single then and elif, no else", expr: ` - set x "Hello" - if $y { - echo "y is true" - } elif $x { - echo "x is true" - }`, want: "x is true\n(nil)\n"}, - {desc: "single then, two elif, and else", expr: ` - set x "Hello" - if $z { - echo "z is true" - } elif $y { - echo "y is true" - } elif $x { - echo "x is true" - }`, want: "x is true\n(nil)\n"}, - {desc: "single then, two elif, and else, expecting else", expr: ` - if $z { - echo "z is true" - } elif $y { - echo "y is true" - } elif $x { - echo "x is true" - } else { - echo "none is true" - }`, want: "none is true\n(nil)\n"}, - {desc: "compressed then", expr: `set x "Hello" ; if $x { echo "true" }`, want: "true\n(nil)\n"}, - {desc: "compressed else", expr: `if $x { echo "true" } else { echo "false" }`, want: "false\n(nil)\n"}, - {desc: "compressed if", expr: `if $x { echo "x" } elif $y { echo "y" } else { echo "false" }`, want: "false\n(nil)\n"}, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - ctx := context.Background() - outW := bytes.NewBuffer(nil) - - inst := cmdlang.New(cmdlang.WithOut(outW), cmdlang.WithTestBuiltin()) - err := inst.EvalAndDisplay(ctx, tt.expr) - - assert.NoError(t, err) - assert.Equal(t, tt.want, outW.String()) - }) - } -} diff --git a/cmdlang/objs.go b/cmdlang/objs.go index 45f26f6..49cb1f5 100644 --- a/cmdlang/objs.go +++ b/cmdlang/objs.go @@ -28,11 +28,24 @@ func toGoValue(obj object) (interface{}, bool) { return nil, true case strObject: return string(v), true + case proxyObject: + return v.p, true } return nil, false } +func fromGoValue(v any) (object, error) { + switch t := v.(type) { + case nil: + return nil, nil + case string: + return strObject(t), nil + default: + return proxyObject{t}, nil + } +} + type macroArgs struct { eval evaluator ec *evalCtx @@ -163,3 +176,16 @@ func isTruthy(obj object) bool { } return obj.Truthy() } + +type proxyObject struct { + p interface{} +} + +func (p proxyObject) String() string { + return fmt.Sprintf("proxyObject{%T}", p.p) +} + +func (p proxyObject) Truthy() bool { + //TODO implement me + panic("implement me") +} diff --git a/cmdlang/testbuiltins_test.go b/cmdlang/testbuiltins_test.go index 2f32af1..8103f33 100644 --- a/cmdlang/testbuiltins_test.go +++ b/cmdlang/testbuiltins_test.go @@ -1,9 +1,12 @@ package cmdlang import ( + "bytes" "context" "fmt" + "github.com/stretchr/testify/assert" "strings" + "testing" ) // Builtins used for test @@ -52,3 +55,120 @@ func WithTestBuiltin() InstOption { i.rootEC.setVar("bee", strObject("buzz")) } } + +func TestBuiltins_Echo(t *testing.T) { + tests := []struct { + desc string + expr string + want string + }{ + {desc: "no args", expr: `echo`, want: "\n"}, + {desc: "single arg", expr: `echo "hello"`, want: "hello\n"}, + {desc: "dual args", expr: `echo "hello " "world"`, want: "hello world\n"}, + {desc: "multi-line 1", expr: ` + echo "Hello" + echo "world" + `, want: "Hello\nworld\n"}, + {desc: "multi-line 2", expr: ` + echo "Hello" + + + echo "world" + `, want: "Hello\nworld\n"}, + {desc: "multi-line 3", expr: ` + +;;; + echo "Hello" +; + + echo "world" +; + `, want: "Hello\nworld\n"}, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + ctx := context.Background() + outW := bytes.NewBuffer(nil) + + inst := New(WithOut(outW), WithTestBuiltin()) + res, err := inst.Eval(ctx, tt.expr) + + assert.NoError(t, err) + assert.Nil(t, res) + assert.Equal(t, tt.want, outW.String()) + }) + } +} + +func TestBuiltins_If(t *testing.T) { + tests := []struct { + desc string + expr string + want string + }{ + {desc: "single then", expr: ` + set x "Hello" + if $x { + echo "true" + }`, want: "true\n(nil)\n"}, + {desc: "single then and else", expr: ` + set x "Hello" + if $x { + echo "true" + } else { + echo "false" + }`, want: "true\n(nil)\n"}, + {desc: "single then, elif and else", expr: ` + set x "Hello" + if $y { + echo "y is true" + } elif $x { + echo "x is true" + } else { + echo "nothings x" + }`, want: "x is true\n(nil)\n"}, + {desc: "single then and elif, no else", expr: ` + set x "Hello" + if $y { + echo "y is true" + } elif $x { + echo "x is true" + }`, want: "x is true\n(nil)\n"}, + {desc: "single then, two elif, and else", expr: ` + set x "Hello" + if $z { + echo "z is true" + } elif $y { + echo "y is true" + } elif $x { + echo "x is true" + }`, want: "x is true\n(nil)\n"}, + {desc: "single then, two elif, and else, expecting else", expr: ` + if $z { + echo "z is true" + } elif $y { + echo "y is true" + } elif $x { + echo "x is true" + } else { + echo "none is true" + }`, want: "none is true\n(nil)\n"}, + {desc: "compressed then", expr: `set x "Hello" ; if $x { echo "true" }`, want: "true\n(nil)\n"}, + {desc: "compressed else", expr: `if $x { echo "true" } else { echo "false" }`, want: "false\n(nil)\n"}, + {desc: "compressed if", expr: `if $x { echo "x" } elif $y { echo "y" } else { echo "false" }`, want: "false\n(nil)\n"}, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + ctx := context.Background() + outW := bytes.NewBuffer(nil) + + inst := New(WithOut(outW), WithTestBuiltin()) + err := inst.EvalAndDisplay(ctx, tt.expr) + + assert.NoError(t, err) + assert.Equal(t, tt.want, outW.String()) + }) + } +} diff --git a/cmdlang/userbuiltin.go b/cmdlang/userbuiltin.go new file mode 100644 index 0000000..d6a24ea --- /dev/null +++ b/cmdlang/userbuiltin.go @@ -0,0 +1,60 @@ +package cmdlang + +import ( + "context" + "errors" + "reflect" +) + +type CallArgs struct { + args invocationArgs +} + +func (ca CallArgs) Bind(vars ...interface{}) error { + if len(ca.args.args) != len(vars) { + return errors.New("wrong number of arguments") + } + + 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 argValue.Elem().Type() != poValue.Type() { + continue + } + + argValue.Elem().Set(poValue) + } + } + return nil +} + +func (inst *Inst) SetBuiltin(name string, fn func(ctx context.Context, args CallArgs) (any, error)) { + inst.rootEC.addCmd(name, userBuiltin{fn: fn}) +} + +type userBuiltin struct { + fn func(ctx context.Context, args CallArgs) (any, error) +} + +func (u userBuiltin) invoke(ctx context.Context, args invocationArgs) (object, error) { + v, err := u.fn(ctx, CallArgs{args: args}) + if err != nil { + return nil, err + } + + return fromGoValue(v) +} diff --git a/cmdlang/userbuiltin_test.go b/cmdlang/userbuiltin_test.go new file mode 100644 index 0000000..d7e350e --- /dev/null +++ b/cmdlang/userbuiltin_test.go @@ -0,0 +1,91 @@ +package cmdlang_test + +import ( + "context" + "github.com/lmika/cmdlang-proto/cmdlang" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestInst_SetBuiltin(t *testing.T) { + t.Run("simple builtin accepting and returning strings", func(t *testing.T) { + inst := cmdlang.New() + inst.SetBuiltin("add2", func(ctx context.Context, args cmdlang.CallArgs) (any, error) { + var x, y string + + if err := args.Bind(&x, &y); err != nil { + return nil, err + } + + return x + y, nil + }) + + res, err := inst.Eval(context.Background(), `add2 "Hello, " "World"`) + assert.NoError(t, err) + assert.Equal(t, "Hello, World", res) + }) + + t.Run("builtin return proxy object", func(t *testing.T) { + type pair struct { + x, y string + } + + inst := cmdlang.New() + inst.SetBuiltin("add2", func(ctx context.Context, args cmdlang.CallArgs) (any, error) { + var x, y string + + if err := args.Bind(&x, &y); err != nil { + return nil, err + } + + return pair{x, y}, nil + }) + + res, err := inst.Eval(context.Background(), `add2 "Hello" "World"`) + assert.NoError(t, err) + assert.Equal(t, pair{"Hello", "World"}, res) + }) + + t.Run("builtin operating on and returning proxy object", func(t *testing.T) { + type pair struct { + x, y string + } + + tests := []struct { + descr string + expr string + want string + }{ + {descr: "pass via args", expr: `join (add2 "left" "right")`, want: "left:right"}, + {descr: "pass via vars", expr: `set x (add2 "blue" "green") ; join $x`, want: "blue:green"}, + } + + for _, tt := range tests { + t.Run(tt.descr, func(t *testing.T) { + inst := cmdlang.New() + inst.SetBuiltin("add2", func(ctx context.Context, args cmdlang.CallArgs) (any, error) { + var x, y string + + if err := args.Bind(&x, &y); err != nil { + return nil, err + } + + return pair{x, y}, nil + }) + inst.SetBuiltin("join", func(ctx context.Context, args cmdlang.CallArgs) (any, error) { + var x pair + + if err := args.Bind(&x); err != nil { + return nil, err + } + + return x.x + ":" + x.y, nil + }) + + res, err := inst.Eval(context.Background(), tt.expr) + assert.NoError(t, err) + assert.Equal(t, tt.want, res) + }) + } + }) +}