From 485e839ae32b15cecef1c356a5246f001efbe777 Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Wed, 15 Jan 2025 22:22:17 +1100 Subject: [PATCH] Added a log builtin --- cmd/cmsh/main.go | 2 ++ cmd/playwasm/jsiter.go | 30 +++++------------- ucl/builtins/log.go | 44 +++++++++++++++++++++++++++ ucl/builtins/log_test.go | 47 ++++++++++++++++++++++++++++ ucl/userbuiltin.go | 66 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 167 insertions(+), 22 deletions(-) create mode 100644 ucl/builtins/log.go create mode 100644 ucl/builtins/log_test.go diff --git a/cmd/cmsh/main.go b/cmd/cmsh/main.go index 8793777..8296224 100644 --- a/cmd/cmsh/main.go +++ b/cmd/cmsh/main.go @@ -20,6 +20,8 @@ func main() { instRepl := repl.New( ucl.WithModule(builtins.OS()), ucl.WithModule(builtins.FS(nil)), + ucl.WithModule(builtins.Log(nil)), + ucl.WithModule(builtins.Strs()), ) ctx := context.Background() diff --git a/cmd/playwasm/jsiter.go b/cmd/playwasm/jsiter.go index dca6b12..e653f4b 100644 --- a/cmd/playwasm/jsiter.go +++ b/cmd/playwasm/jsiter.go @@ -8,6 +8,7 @@ import ( "github.com/alecthomas/participle/v2" "strings" "syscall/js" + "ucl.lmika.dev/repl" "ucl.lmika.dev/ucl" ) @@ -20,9 +21,12 @@ func invokeUCLCallback(name string, args ...any) { func initJS(ctx context.Context) { uclObj := make(map[string]any) - inst := ucl.New(ucl.WithOut(ucl.LineHandler(func(line string) { - invokeUCLCallback("onOutLine", line) - }))) + replInst := repl.New( + ucl.WithModule(builtins.Log(nil)), + ucl.WithModule(builtins.Strs()), + 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 { @@ -36,7 +40,7 @@ func initJS(ctx context.Context) { } wantContinue := args[1].Bool() - if err := ucl.EvalAndDisplay(ctx, inst, cmdLine); err != nil { + if err := replInst.EvalAndDisplay(ctx, cmdLine); err != nil { var p participle.Error if errors.As(err, &p) && wantContinue { invokeUCLCallback("onContinue") @@ -50,21 +54,3 @@ 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 -//} diff --git a/ucl/builtins/log.go b/ucl/builtins/log.go new file mode 100644 index 0000000..c336833 --- /dev/null +++ b/ucl/builtins/log.go @@ -0,0 +1,44 @@ +package builtins + +import ( + "context" + "log" + "strings" + "ucl.lmika.dev/ucl" +) + +type LogMessageHandler func(s string) error + +func Log(handler LogMessageHandler) ucl.Module { + if handler == nil { + handler = func(s string) error { + log.Println(s) + return nil + } + } + + lh := logHandler{handler: handler} + + return ucl.Module{ + Name: "log", + Builtins: map[string]ucl.BuiltinHandler{ + "puts": lh.logMessage, + }, + } +} + +type logHandler struct { + handler LogMessageHandler +} + +func (lh logHandler) logMessage(ctx context.Context, args ucl.CallArgs) (any, error) { + var sb strings.Builder + var s ucl.Object + + for args.NArgs() > 0 { + if err := args.Bind(&s); err == nil { + sb.WriteString(s.String()) + } + } + return nil, lh.handler(sb.String()) +} diff --git a/ucl/builtins/log_test.go b/ucl/builtins/log_test.go new file mode 100644 index 0000000..9071c02 --- /dev/null +++ b/ucl/builtins/log_test.go @@ -0,0 +1,47 @@ +package builtins_test + +import ( + "bytes" + "context" + "github.com/stretchr/testify/assert" + "testing" + "ucl.lmika.dev/ucl" + "ucl.lmika.dev/ucl/builtins" +) + +func TestLog_Puts(t *testing.T) { + tests := []struct { + desc string + eval string + want any + wantErr bool + wantLog string + }{ + {desc: "log 1", eval: `log:puts "bad stuff here"`, wantLog: "bad stuff here\n"}, + {desc: "log 2", eval: `log:puts "bad " 123 " here " [1 2 3]`, wantLog: "bad 123 here [1 2 3]\n"}, + {desc: "log 3", eval: `log:puts`, wantLog: "\n"}, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + var logMessage bytes.Buffer + + inst := ucl.New( + ucl.WithModule(builtins.Log(func(s string) error { + logMessage.WriteString(s) + logMessage.WriteByte('\n') + return nil + })), + ) + + res, err := inst.Eval(context.Background(), tt.eval) + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.want, res) + } + assert.Equal(t, tt.wantLog, logMessage.String()) + }) + } +} diff --git a/ucl/userbuiltin.go b/ucl/userbuiltin.go index d9f880b..c432e6a 100644 --- a/ucl/userbuiltin.go +++ b/ucl/userbuiltin.go @@ -94,20 +94,44 @@ func (u userBuiltin) invoke(ctx context.Context, args invocationArgs) (object, e func bindArg(v interface{}, arg object) error { switch t := v.(type) { + case *Object: + *t = arg + return nil case *interface{}: *t, _ = toGoValue(arg) + case *Invokable: + i, ok := arg.(invokable) + if !ok { + return errors.New("exepected invokable") + } + *t = Invokable{ + inv: i, + eval: ca.args.eval, + inst: ca.args.inst, + ec: ca.args.ec, + } + return nil + case *Listable: + i, ok := arg.(Listable) + if !ok { + return errors.New("exepected listable") + } + *t = i + return nil case *string: if arg != nil { *t = arg.String() } else { *t = "" } + return nil case *int: if iArg, ok := arg.(intObject); ok { *t = int(iArg) } else { return errors.New("invalid arg") } + return nil } switch t := arg.(type) { @@ -198,3 +222,45 @@ func (m missingHandlerInvokable) invoke(ctx context.Context, args invocationArgs return fromGoValue(v) } + +type Invokable struct { + inv invokable + eval evaluator + inst *Inst + ec *evalCtx +} + +func (i Invokable) IsNil() bool { + return i.inv == nil +} + +func (i Invokable) Invoke(ctx context.Context, args ...any) (any, error) { + if i.inv == nil { + return nil, nil + } + + var err error + invArgs := invocationArgs{ + eval: i.eval, + ec: i.ec, + inst: i.inst, + } + + invArgs.args, err = slices.MapWithError(args, func(a any) (Object, error) { + return fromGoValue(a) + }) + if err != nil { + return nil, err + } + + res, err := i.inv.invoke(ctx, invArgs) + if err != nil { + return nil, err + } + + goRes, ok := toGoValue(res) + if !ok { + return nil, errors.New("cannot convert result to Go Value") + } + return goRes, err +}