diff --git a/cmd/playwasm/jsiter.go b/cmd/playwasm/jsiter.go index 1005a83..dca6b12 100644 --- a/cmd/playwasm/jsiter.go +++ b/cmd/playwasm/jsiter.go @@ -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 +//} diff --git a/ucl/eval.go b/ucl/eval.go index ce14fe2..56e8ce0 100644 --- a/ucl/eval.go +++ b/ucl/eval.go @@ -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) } diff --git a/ucl/inst.go b/ucl/inst.go index 65f0c90..0eef348 100644 --- a/ucl/inst.go +++ b/ucl/inst.go @@ -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 diff --git a/ucl/io.go b/ucl/io.go new file mode 100644 index 0000000..8e294bd --- /dev/null +++ b/ucl/io.go @@ -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 +} diff --git a/ucl/userbuiltin.go b/ucl/userbuiltin.go index 7811b8c..2477308 100644 --- a/ucl/userbuiltin.go +++ b/ucl/userbuiltin.go @@ -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) +} diff --git a/ucl/userbuiltin_test.go b/ucl/userbuiltin_test.go index 1e48320..3778e76 100644 --- a/ucl/userbuiltin_test.go +++ b/ucl/userbuiltin_test.go @@ -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) {