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

View File

@ -9,7 +9,8 @@ import (
) )
type Inst struct { type Inst struct {
out io.Writer out io.Writer
missingBuiltinHandler MissingBuiltinHandler
rootEC *evalCtx 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 { func New(opts ...InstOption) *Inst {
rootEC := &evalCtx{} rootEC := &evalCtx{}
rootEC.root = rootEC 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" "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 { type CallArgs struct {
args invocationArgs args invocationArgs
} }
func (ca *CallArgs) NArgs() int {
return len(ca.args.args)
}
func (ca *CallArgs) Bind(vars ...interface{}) error { func (ca *CallArgs) Bind(vars ...interface{}) error {
if len(ca.args.args) < len(vars) { if len(ca.args.args) < len(vars) {
return errors.New("wrong number of arguments") 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]) 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}) inst.rootEC.addCmd(name, userBuiltin{fn: fn})
} }
@ -166,3 +174,21 @@ func canBindProxyObject(v interface{}, r reflect.Value) bool {
r = r.Elem() 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 ( import (
"bytes" "bytes"
"context" "context"
"fmt"
"strings" "strings"
"testing" "testing"
"ucl.lmika.dev/ucl" "ucl.lmika.dev/ucl"
@ -184,90 +185,117 @@ func TestInst_SetBuiltin(t *testing.T) {
} }
func TestCallArgs_Bind(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 := ucl.New()
inst.SetBuiltin("sa", func(ctx context.Context, args ucl.CallArgs) (any, error) { inst.SetBuiltin("sa", func(ctx context.Context, args ucl.CallArgs) (any, error) {
return doStringA{this: "a val"}, nil 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.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) { 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 {
tests := []struct { descr string
descr string eval string
eval string want []string
want []string }{
}{ {descr: "bind nothing", eval: `test`, want: []string{}},
{descr: "bind nothing", eval: `test`, want: []string{}}, {descr: "bind one", eval: `test "yes"`, want: []string{"str"}},
{descr: "bind one", eval: `test "yes"`, want: []string{"str"}}, {descr: "bind two", eval: `test "yes" 213`, want: []string{"str", "int"}},
{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"}},
{descr: "bind three", eval: `test "yes" 213 (proxy)`, want: []string{"all", "str", "int", "proxy"}}, }
}
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.descr, func(t *testing.T) { t.Run(tt.descr, func(t *testing.T) {
type proxyObj struct{} type proxyObj struct{}
ctx := context.Background() ctx := context.Background()
res := make([]string, 0) res := make([]string, 0)
inst := ucl.New() inst := ucl.New()
inst.SetBuiltin("proxy", func(ctx context.Context, args ucl.CallArgs) (any, error) { inst.SetBuiltin("proxy", func(ctx context.Context, args ucl.CallArgs) (any, error) {
return proxyObj{}, nil 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.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) { func TestCallArgs_IsTopLevel(t *testing.T) {