Started working on the public interface
This commit is contained in:
parent
d6cc449b40
commit
219d4d49da
|
@ -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())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -28,11 +28,24 @@ func toGoValue(obj object) (interface{}, bool) {
|
||||||
return nil, true
|
return nil, true
|
||||||
case strObject:
|
case strObject:
|
||||||
return string(v), true
|
return string(v), true
|
||||||
|
case proxyObject:
|
||||||
|
return v.p, true
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, false
|
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 {
|
type macroArgs struct {
|
||||||
eval evaluator
|
eval evaluator
|
||||||
ec *evalCtx
|
ec *evalCtx
|
||||||
|
@ -163,3 +176,16 @@ func isTruthy(obj object) bool {
|
||||||
}
|
}
|
||||||
return obj.Truthy()
|
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")
|
||||||
|
}
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
package cmdlang
|
package cmdlang
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
"strings"
|
"strings"
|
||||||
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Builtins used for test
|
// Builtins used for test
|
||||||
|
@ -52,3 +55,120 @@ func WithTestBuiltin() InstOption {
|
||||||
i.rootEC.setVar("bee", strObject("buzz"))
|
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())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
60
cmdlang/userbuiltin.go
Normal file
60
cmdlang/userbuiltin.go
Normal file
|
@ -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)
|
||||||
|
}
|
91
cmdlang/userbuiltin_test.go
Normal file
91
cmdlang/userbuiltin_test.go
Normal file
|
@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
Loading…
Reference in a new issue