Added a few things needed by Dynamo Browse

- Added a NArg returning the total number of arguments
- Added a "missing command" handler
- Added a utility writer which will print out lines as they're collected
This commit is contained in:
Leon Mika 2024-05-01 21:05:14 +10:00
parent 57781b5e3b
commit 25594c80d2
6 changed files with 190 additions and 100 deletions

View file

@ -3,7 +3,6 @@
package main
import (
"bytes"
"context"
"errors"
"github.com/alecthomas/participle/v2"
@ -21,12 +20,9 @@ func invokeUCLCallback(name string, args ...any) {
func initJS(ctx context.Context) {
uclObj := make(map[string]any)
inst := ucl.New(ucl.WithOut(&uclOut{
lineBuffer: new(bytes.Buffer),
writeLine: func(line string) {
invokeUCLCallback("onOutLine", line)
},
}))
inst := ucl.New(ucl.WithOut(ucl.LineHandler(func(line string) {
invokeUCLCallback("onOutLine", line)
})))
uclObj["eval"] = js.FuncOf(func(this js.Value, args []js.Value) any {
if len(args) != 2 {
@ -55,19 +51,20 @@ func initJS(ctx context.Context) {
js.Global().Set("ucl", uclObj)
}
type uclOut struct {
lineBuffer *bytes.Buffer
writeLine func(line string)
}
func (uo *uclOut) Write(p []byte) (n int, err error) {
for _, b := range p {
if b == '\n' {
uo.writeLine(uo.lineBuffer.String())
uo.lineBuffer.Reset()
} else {
uo.lineBuffer.WriteByte(b)
}
}
return len(p), nil
}
//
//type uclOut struct {
// lineBuffer *bytes.Buffer
// writeLine func(line string)
//}
//
//func (uo *uclOut) Write(p []byte) (n int, err error) {
// for _, b := range p {
// if b == '\n' {
// uo.writeLine(uo.lineBuffer.String())
// uo.lineBuffer.Reset()
// } else {
// uo.lineBuffer.WriteByte(b)
// }
// }
// return len(p), nil
//}

View file

@ -79,6 +79,8 @@ func (e evaluator) evalCmd(ctx context.Context, ec *evalCtx, currentPipe object,
return e.evalInvokable(ctx, ec, currentPipe, ast, cmd)
} else if macro := ec.lookupMacro(name); macro != nil {
return e.evalMacro(ctx, ec, currentPipe != nil, currentPipe, ast, macro)
} else if missingHandler := e.inst.missingBuiltinHandler; missingHandler != nil {
return e.evalInvokable(ctx, ec, currentPipe, ast, e.inst.missingHandlerInvokable(name))
} else {
return nil, errors.New("unknown command: " + name)
}

View file

@ -9,7 +9,8 @@ import (
)
type Inst struct {
out io.Writer
out io.Writer
missingBuiltinHandler MissingBuiltinHandler
rootEC *evalCtx
}
@ -22,6 +23,12 @@ func WithOut(out io.Writer) InstOption {
}
}
func WithMissingBuiltinHandler(handler MissingBuiltinHandler) InstOption {
return func(i *Inst) {
i.missingBuiltinHandler = handler
}
}
func New(opts ...InstOption) *Inst {
rootEC := &evalCtx{}
rootEC.root = rootEC

30
ucl/io.go Normal file
View file

@ -0,0 +1,30 @@
package ucl
import (
"bytes"
"io"
)
func LineHandler(line func(string)) io.Writer {
return &lineHandlerWriter{
lineBuffer: new(bytes.Buffer),
writeLine: line,
}
}
type lineHandlerWriter struct {
lineBuffer *bytes.Buffer
writeLine func(line string)
}
func (uo *lineHandlerWriter) Write(p []byte) (n int, err error) {
for _, b := range p {
if b == '\n' {
uo.writeLine(uo.lineBuffer.String())
uo.lineBuffer.Reset()
} else {
uo.lineBuffer.WriteByte(b)
}
}
return len(p), nil
}

View file

@ -6,10 +6,18 @@ import (
"reflect"
)
type BuiltinHandler func(ctx context.Context, args CallArgs) (any, error)
type MissingBuiltinHandler func(ctx context.Context, name string, args CallArgs) (any, error)
type CallArgs struct {
args invocationArgs
}
func (ca *CallArgs) NArgs() int {
return len(ca.args.args)
}
func (ca *CallArgs) Bind(vars ...interface{}) error {
if len(ca.args.args) < len(vars) {
return errors.New("wrong number of arguments")
@ -67,7 +75,7 @@ func (ca CallArgs) BindSwitch(name string, val interface{}) error {
return bindArg(val, (*vars)[0])
}
func (inst *Inst) SetBuiltin(name string, fn func(ctx context.Context, args CallArgs) (any, error)) {
func (inst *Inst) SetBuiltin(name string, fn BuiltinHandler) {
inst.rootEC.addCmd(name, userBuiltin{fn: fn})
}
@ -166,3 +174,21 @@ func canBindProxyObject(v interface{}, r reflect.Value) bool {
r = r.Elem()
}
}
func (inst *Inst) missingHandlerInvokable(name string) missingHandlerInvokable {
return missingHandlerInvokable{name: name, handler: inst.missingBuiltinHandler}
}
type missingHandlerInvokable struct {
name string
handler MissingBuiltinHandler
}
func (m missingHandlerInvokable) invoke(ctx context.Context, args invocationArgs) (object, error) {
v, err := m.handler(ctx, m.name, CallArgs{args: args})
if err != nil {
return nil, err
}
return fromGoValue(v)
}

View file

@ -3,6 +3,7 @@ package ucl_test
import (
"bytes"
"context"
"fmt"
"strings"
"testing"
"ucl.lmika.dev/ucl"
@ -184,90 +185,117 @@ func TestInst_SetBuiltin(t *testing.T) {
}
func TestCallArgs_Bind(t *testing.T) {
t.Run("bind to an interface", func(t *testing.T) {
ctx := context.Background()
ctx := context.Background()
inst := ucl.New()
inst.SetBuiltin("sa", func(ctx context.Context, args ucl.CallArgs) (any, error) {
return doStringA{this: "a val"}, nil
})
inst.SetBuiltin("sb", func(ctx context.Context, args ucl.CallArgs) (any, error) {
return doStringB{left: "foo", right: "bar"}, nil
})
inst.SetBuiltin("dostr", func(ctx context.Context, args ucl.CallArgs) (any, error) {
var ds doStringable
if err := args.Bind(&ds); err != nil {
return nil, err
}
return ds.DoString(), nil
})
va, err := inst.Eval(ctx, `dostr (sa)`)
assert.NoError(t, err)
assert.Equal(t, "do string A: a val", va)
vb, err := inst.Eval(ctx, `dostr (sb)`)
assert.NoError(t, err)
assert.Equal(t, "do string B: foo bar", vb)
inst := ucl.New()
inst.SetBuiltin("sa", func(ctx context.Context, args ucl.CallArgs) (any, error) {
return doStringA{this: "a val"}, nil
})
inst.SetBuiltin("sb", func(ctx context.Context, args ucl.CallArgs) (any, error) {
return doStringB{left: "foo", right: "bar"}, nil
})
inst.SetBuiltin("dostr", func(ctx context.Context, args ucl.CallArgs) (any, error) {
var ds doStringable
if err := args.Bind(&ds); err != nil {
return nil, err
}
return ds.DoString(), nil
})
va, err := inst.Eval(ctx, `dostr (sa)`)
assert.NoError(t, err)
assert.Equal(t, "do string A: a val", va)
vb, err := inst.Eval(ctx, `dostr (sb)`)
assert.NoError(t, err)
assert.Equal(t, "do string B: foo bar", vb)
}
func TestCallArgs_CanBind(t *testing.T) {
t.Run("returns ture of all passed in arguments can be bound without consuming them", func(t *testing.T) {
tests := []struct {
descr string
eval string
want []string
}{
{descr: "bind nothing", eval: `test`, want: []string{}},
{descr: "bind one", eval: `test "yes"`, want: []string{"str"}},
{descr: "bind two", eval: `test "yes" 213`, want: []string{"str", "int"}},
{descr: "bind three", eval: `test "yes" 213 (proxy)`, want: []string{"all", "str", "int", "proxy"}},
}
tests := []struct {
descr string
eval string
want []string
}{
{descr: "bind nothing", eval: `test`, want: []string{}},
{descr: "bind one", eval: `test "yes"`, want: []string{"str"}},
{descr: "bind two", eval: `test "yes" 213`, want: []string{"str", "int"}},
{descr: "bind three", eval: `test "yes" 213 (proxy)`, want: []string{"all", "str", "int", "proxy"}},
}
for _, tt := range tests {
t.Run(tt.descr, func(t *testing.T) {
type proxyObj struct{}
for _, tt := range tests {
t.Run(tt.descr, func(t *testing.T) {
type proxyObj struct{}
ctx := context.Background()
res := make([]string, 0)
ctx := context.Background()
res := make([]string, 0)
inst := ucl.New()
inst.SetBuiltin("proxy", func(ctx context.Context, args ucl.CallArgs) (any, error) {
return proxyObj{}, nil
})
inst.SetBuiltin("test", func(ctx context.Context, args ucl.CallArgs) (any, error) {
var (
s string
i int
p proxyObj
)
if args.CanBind(&s, &i, &p) {
res = append(res, "all")
}
if args.CanBind(&s) {
res = append(res, "str")
}
args.Shift(1)
if args.CanBind(&i) {
res = append(res, "int")
}
args.Shift(1)
if args.CanBind(&p) {
res = append(res, "proxy")
}
return nil, nil
})
_, err := inst.Eval(ctx, tt.eval)
assert.NoError(t, err)
assert.Equal(t, tt.want, res)
inst := ucl.New()
inst.SetBuiltin("proxy", func(ctx context.Context, args ucl.CallArgs) (any, error) {
return proxyObj{}, nil
})
}
})
inst.SetBuiltin("test", func(ctx context.Context, args ucl.CallArgs) (any, error) {
var (
s string
i int
p proxyObj
)
if args.CanBind(&s, &i, &p) {
res = append(res, "all")
}
if args.CanBind(&s) {
res = append(res, "str")
}
args.Shift(1)
if args.CanBind(&i) {
res = append(res, "int")
}
args.Shift(1)
if args.CanBind(&p) {
res = append(res, "proxy")
}
return nil, nil
})
_, err := inst.Eval(ctx, tt.eval)
assert.NoError(t, err)
assert.Equal(t, tt.want, res)
})
}
}
func TestCallArgs_MissingCommandHandler(t *testing.T) {
tests := []struct {
descr string
eval string
want string
}{
{descr: "alpha", eval: `alpha`, want: "was alpha"},
{descr: "bravo", eval: `bravo "this"`, want: "was bravo: this"},
{descr: "charlie", eval: `charlie`, want: "was charlie"},
}
for _, tt := range tests {
t.Run(tt.descr, func(t *testing.T) {
ctx := context.Background()
inst := ucl.New(
ucl.WithMissingBuiltinHandler(func(ctx context.Context, name string, args ucl.CallArgs) (any, error) {
var msg string
if err := args.Bind(&msg); err == nil {
return fmt.Sprintf("was %v: %v", name, msg), nil
}
return fmt.Sprintf("was %v", name), nil
}))
res, err := inst.Eval(ctx, tt.eval)
assert.NoError(t, err)
assert.Equal(t, tt.want, res)
})
}
}
func TestCallArgs_IsTopLevel(t *testing.T) {