diff --git a/internal/dynamo-browse/services/scriptmanager/builtins.go b/internal/dynamo-browse/services/scriptmanager/builtins.go new file mode 100644 index 0000000..9674c99 --- /dev/null +++ b/internal/dynamo-browse/services/scriptmanager/builtins.go @@ -0,0 +1,55 @@ +/** + * Builtins adopted and modified from Taramin + * Copyright (c) 2022 Curtis Myzie + */ + +package scriptmanager + +import ( + "context" + "github.com/cloudcmds/tamarin/object" + "log" +) + +func printBuiltin(ctx context.Context, args ...object.Object) object.Object { + env := scriptEnvFromCtx(ctx) + prefix := "script " + env.filename + ":" + + values := make([]interface{}, len(args)+1) + values[0] = prefix + for i, arg := range args { + switch arg := arg.(type) { + case *object.String: + values[i+1] = arg.Value() + default: + values[i+1] = arg.Inspect() + } + } + log.Println(values...) + return object.Nil +} + +func printfBuiltin(ctx context.Context, args ...object.Object) object.Object { + env := scriptEnvFromCtx(ctx) + prefix := "script " + env.filename + ":" + + numArgs := len(args) + if numArgs < 1 { + return object.Errorf("type error: printf() takes 1 or more arguments (%d given)", len(args)) + } + format, err := object.AsString(args[0]) + if err != nil { + return err + } + var values = []interface{}{prefix} + for _, arg := range args[1:] { + switch arg := arg.(type) { + case *object.String: + values = append(values, arg.Value()) + default: + values = append(values, arg.Interface()) + } + } + log.Printf("%s "+format, values...) + return object.Nil +} diff --git a/internal/dynamo-browse/services/scriptmanager/modext.go b/internal/dynamo-browse/services/scriptmanager/modext.go index f2f4f0c..c581d17 100644 --- a/internal/dynamo-browse/services/scriptmanager/modext.go +++ b/internal/dynamo-browse/services/scriptmanager/modext.go @@ -31,6 +31,8 @@ func (m *extModule) register(scp *scope.Scope) { } func (m *extModule) command(ctx context.Context, args ...object.Object) object.Object { + thisEnv := scriptEnvFromCtx(ctx) + if err := arg.Require("ext.command", 2, args); err != nil { return err } @@ -56,7 +58,9 @@ func (m *extModule) command(ctx context.Context, args ...object.Object) object.O objArgs[i] = object.NewString(a) } - ctx = ctxWithOptions(ctx, m.scriptPlugin.scriptService.options) + newEnv := thisEnv + newEnv.options = m.scriptPlugin.scriptService.options + ctx = ctxWithScriptEnv(ctx, newEnv) res := callFn(ctx, fnRes.Scope(), fnRes, objArgs) if object.IsError(res) { @@ -74,6 +78,8 @@ func (m *extModule) command(ctx context.Context, args ...object.Object) object.O } func (m *extModule) keyBinding(ctx context.Context, args ...object.Object) object.Object { + thisEnv := scriptEnvFromCtx(ctx) + if err := arg.Require("ext.key_binding", 3, args); err != nil { return err } @@ -112,7 +118,9 @@ func (m *extModule) keyBinding(ctx context.Context, args ...object.Object) objec objArgs[i] = object.NewString(a) } - ctx = ctxWithOptions(ctx, m.scriptPlugin.scriptService.options) + newEnv := thisEnv + newEnv.options = m.scriptPlugin.scriptService.options + ctx = ctxWithScriptEnv(ctx, newEnv) res := callFn(ctx, fnRes.Scope(), fnRes, objArgs) if object.IsError(res) { diff --git a/internal/dynamo-browse/services/scriptmanager/modos.go b/internal/dynamo-browse/services/scriptmanager/modos.go index 7308467..b050fc4 100644 --- a/internal/dynamo-browse/services/scriptmanager/modos.go +++ b/internal/dynamo-browse/services/scriptmanager/modos.go @@ -22,7 +22,7 @@ func (om *osModule) exec(ctx context.Context, args ...object.Object) object.Obje return objErr } - opts := optionFromCtx(ctx) + opts := scriptEnvFromCtx(ctx).options if !opts.Permissions.AllowShellCommands { return object.NewErrResult(object.Errorf("permission error: no permission to shell out")) } @@ -46,7 +46,7 @@ func (om *osModule) env(ctx context.Context, args ...object.Object) object.Objec return objErr } - opts := optionFromCtx(ctx) + opts := scriptEnvFromCtx(ctx).options if !opts.Permissions.AllowEnv { return object.Nil } diff --git a/internal/dynamo-browse/services/scriptmanager/modsession_test.go b/internal/dynamo-browse/services/scriptmanager/modsession_test.go index 97ecc4e..c28bade 100644 --- a/internal/dynamo-browse/services/scriptmanager/modsession_test.go +++ b/internal/dynamo-browse/services/scriptmanager/modsession_test.go @@ -39,12 +39,12 @@ func TestModSession_Table(t *testing.T) { table := session.current_table() assert(table.name == "test_table") - assert(table.keys["partition"] == "pk") - assert(table.keys["sort"] == "sk") + assert(table.keys["hash"] == "pk") + assert(table.keys["range"] == "sk") assert(len(table.gsis) == 1) assert(table.gsis[0].name == "index-1") - assert(table.gsis[0].keys["partition"] == "ipk") - assert(table.gsis[0].keys["sort"] == "isk") + assert(table.gsis[0].keys["hash"] == "ipk") + assert(table.gsis[0].keys["range"] == "isk") assert(table == session.result_set().table) `) diff --git a/internal/dynamo-browse/services/scriptmanager/modui.go b/internal/dynamo-browse/services/scriptmanager/modui.go index 75cdd12..081eaa5 100644 --- a/internal/dynamo-browse/services/scriptmanager/modui.go +++ b/internal/dynamo-browse/services/scriptmanager/modui.go @@ -40,7 +40,7 @@ func (um *uiModule) prompt(ctx context.Context, args ...object.Object) object.Ob if hasResp { return object.NewString(resp) } else { - return object.NewError(ctx.Err()) + return object.Nil } case <-ctx.Done(): return object.NewError(ctx.Err()) diff --git a/internal/dynamo-browse/services/scriptmanager/modui_test.go b/internal/dynamo-browse/services/scriptmanager/modui_test.go index 10a2a1c..7fa264e 100644 --- a/internal/dynamo-browse/services/scriptmanager/modui_test.go +++ b/internal/dynamo-browse/services/scriptmanager/modui_test.go @@ -39,11 +39,12 @@ func TestModUI_Prompt(t *testing.T) { mockedUIService.AssertExpectations(t) }) - t.Run("should return error if prompt was cancelled", func(t *testing.T) { + t.Run("should return nil if prompt was cancelled", func(t *testing.T) { testFS := testScriptFile(t, "test.tm", ` ui.print("Hello, world") var name = ui.prompt("What is your name? ") ui.print("After") + ui.print(nil) `) promptChan := make(chan string) @@ -53,6 +54,8 @@ func TestModUI_Prompt(t *testing.T) { mockedUIService := mocks.NewUIService(t) mockedUIService.EXPECT().PrintMessage(mock.Anything, "Hello, world") + mockedUIService.EXPECT().PrintMessage(mock.Anything, "After") + mockedUIService.EXPECT().PrintMessage(mock.Anything, "nil") mockedUIService.EXPECT().Prompt(mock.Anything, "What is your name? ").Return(promptChan) srv := scriptmanager.New(scriptmanager.WithFS(testFS)) @@ -61,9 +64,8 @@ func TestModUI_Prompt(t *testing.T) { }) err := <-srv.RunAdHocScript(ctx, "test.tm") - assert.Error(t, err) + assert.NoError(t, err) - mockedUIService.AssertNotCalled(t, "Prompt", "after") mockedUIService.AssertExpectations(t) }) diff --git a/internal/dynamo-browse/services/scriptmanager/opts.go b/internal/dynamo-browse/services/scriptmanager/opts.go index 06f4330..9d8e489 100644 --- a/internal/dynamo-browse/services/scriptmanager/opts.go +++ b/internal/dynamo-browse/services/scriptmanager/opts.go @@ -34,15 +34,21 @@ type Permissions struct { AllowEnv bool } -type optionCtxKeyType struct{} +// scriptEnv is the runtime environment for a particular script execution +type scriptEnv struct { + filename string + options Options +} -var optionCtxKey = optionCtxKeyType{} +type scriptEnvKeyType struct{} -func optionFromCtx(ctx context.Context) Options { - perms, _ := ctx.Value(optionCtxKey).(Options) +var scriptEnvKey = scriptEnvKeyType{} + +func scriptEnvFromCtx(ctx context.Context) scriptEnv { + perms, _ := ctx.Value(scriptEnvKey).(scriptEnv) return perms } -func ctxWithOptions(ctx context.Context, perms Options) context.Context { - return context.WithValue(ctx, optionCtxKey, perms) +func ctxWithScriptEnv(ctx context.Context, perms scriptEnv) context.Context { + return context.WithValue(ctx, scriptEnvKey, perms) } diff --git a/internal/dynamo-browse/services/scriptmanager/resultsetproxy.go b/internal/dynamo-browse/services/scriptmanager/resultsetproxy.go index 92d8dca..ed169ff 100644 --- a/internal/dynamo-browse/services/scriptmanager/resultsetproxy.go +++ b/internal/dynamo-browse/services/scriptmanager/resultsetproxy.go @@ -177,27 +177,6 @@ func (i *itemProxy) value(ctx context.Context, args ...object.Object) object.Obj return object.NewError(err) } return tVal - - // TODO - //switch v := av.(type) { - //case *types.AttributeValueMemberS: - // return object.NewString(v.Value) - //case *types.AttributeValueMemberN: - // // TODO: better - // f, err := strconv.ParseFloat(v.Value, 64) - // if err != nil { - // return object.NewError(errors.Errorf("value error: invalid N value: %v", v.Value)) - // } - // return object.NewFloat(f) - //case *types.AttributeValueMemberBOOL: - // if v.Value { - // return object.True - // } - // return object.False - //case *types.AttributeValueMemberNULL: - // return object.Nil - //} - //return object.NewError(errors.New("TODO")) } func (i *itemProxy) setValue(ctx context.Context, args ...object.Object) object.Object { diff --git a/internal/dynamo-browse/services/scriptmanager/service.go b/internal/dynamo-browse/services/scriptmanager/service.go index 7d68d28..30d4f7a 100644 --- a/internal/dynamo-browse/services/scriptmanager/service.go +++ b/internal/dynamo-browse/services/scriptmanager/service.go @@ -3,10 +3,12 @@ package scriptmanager import ( "context" "github.com/cloudcmds/tamarin/exec" + "github.com/cloudcmds/tamarin/object" "github.com/cloudcmds/tamarin/scope" "github.com/lmika/audax/internal/dynamo-browse/services/keybindings" "github.com/pkg/errors" "io/fs" + "log" "os" "path/filepath" "strings" @@ -86,7 +88,7 @@ func (s *Service) StartAdHocScript(ctx context.Context, filename string, errChan func (s *Service) startAdHocScript(ctx context.Context, filename string, errChan chan error) { defer close(errChan) - code, err := s.readScript(filename) + code, err := s.readScript(filename, true) if err != nil { errChan <- errors.Wrapf(err, "cannot load script file %v", filename) return @@ -94,12 +96,16 @@ func (s *Service) startAdHocScript(ctx context.Context, filename string, errChan scp := scope.New(scope.Opts{Parent: s.parentScope()}) - ctx = ctxWithOptions(ctx, s.options) + ctx = ctxWithScriptEnv(ctx, scriptEnv{filename: filepath.Base(filename), options: s.options}) if _, err = exec.Execute(ctx, exec.Opts{ Input: string(code), File: filename, Scope: scp, + Builtins: []*object.Builtin{ + object.NewBuiltin("print", printBuiltin), + object.NewBuiltin("printf", printfBuiltin), + }, }); err != nil { errChan <- errors.Wrapf(err, "script %v", filename) return @@ -114,7 +120,7 @@ type loadedScriptResult struct { func (s *Service) loadScript(ctx context.Context, filename string, resChan chan loadedScriptResult) { defer close(resChan) - code, err := s.readScript(filename) + code, err := s.readScript(filename, false) if err != nil { resChan <- loadedScriptResult{err: errors.Wrapf(err, "cannot load script file %v", filename)} return @@ -129,7 +135,7 @@ func (s *Service) loadScript(ctx context.Context, filename string, resChan chan (&extModule{scriptPlugin: newPlugin}).register(scp) - ctx = ctxWithOptions(ctx, s.options) + ctx = ctxWithScriptEnv(ctx, scriptEnv{filename: filepath.Base(filename), options: s.options}) if _, err = exec.Execute(ctx, exec.Opts{ Input: string(code), @@ -143,8 +149,33 @@ func (s *Service) loadScript(ctx context.Context, filename string, resChan chan resChan <- loadedScriptResult{scriptPlugin: newPlugin} } -func (s *Service) readScript(filename string) ([]byte, error) { +func (s *Service) readScript(filename string, allowCwd bool) ([]byte, error) { + if allowCwd { + if cwd, err := os.Getwd(); err == nil { + fullScriptPath := filepath.Join(cwd, filename) + log.Printf("checking %v", fullScriptPath) + if stat, err := os.Stat(fullScriptPath); err == nil && !stat.IsDir() { + code, err := os.ReadFile(filename) + if err != nil { + return nil, err + } + return code, nil + } + } else { + log.Printf("warn: cannot get cwd for reading script %v: %v", filename, err) + } + } + + if strings.HasPrefix(filename, string(filepath.Separator)) { + code, err := os.ReadFile(filename) + if err != nil { + return nil, err + } + return code, nil + } + for _, currFS := range s.lookupPaths { + log.Printf("checking %v/%v", currFS, filename) stat, err := fs.Stat(currFS, filename) if err != nil { if errors.Is(err, os.ErrNotExist) { diff --git a/internal/dynamo-browse/services/scriptmanager/tableproxy.go b/internal/dynamo-browse/services/scriptmanager/tableproxy.go index 782f837..1ef6204 100644 --- a/internal/dynamo-browse/services/scriptmanager/tableproxy.go +++ b/internal/dynamo-browse/services/scriptmanager/tableproxy.go @@ -8,8 +8,8 @@ import ( ) const ( - tableProxyPartitionKey = "partition" - tableProxySortKey = "sort" + tableProxyPartitionKey = "hash" + tableProxySortKey = "range" ) type tableProxy struct { @@ -42,6 +42,13 @@ func (t *tableProxy) GetAttr(name string) (object.Object, bool) { case "name": return object.NewString(t.table.Name), true case "keys": + if t.table.Keys.SortKey == "" { + return object.NewMap(map[string]object.Object{ + tableProxyPartitionKey: object.NewString(t.table.Keys.PartitionKey), + tableProxySortKey: object.Nil, + }), true + } + return object.NewMap(map[string]object.Object{ tableProxyPartitionKey: object.NewString(t.table.Keys.PartitionKey), tableProxySortKey: object.NewString(t.table.Keys.SortKey), @@ -66,11 +73,11 @@ func newTableIndexProxy(gsi models.TableGSI) object.Object { } func (t tableIndexProxy) Type() object.Type { - return "index" + return "table_index" } func (t tableIndexProxy) Inspect() string { - return "index(gsi," + t.gsi.Name + ")" + return "table_index(gsi," + t.gsi.Name + ")" } func (t tableIndexProxy) Interface() interface{} { diff --git a/internal/dynamo-browse/ui/teamodels/statusandprompt/model.go b/internal/dynamo-browse/ui/teamodels/statusandprompt/model.go index c25c560..62be7b0 100644 --- a/internal/dynamo-browse/ui/teamodels/statusandprompt/model.go +++ b/internal/dynamo-browse/ui/teamodels/statusandprompt/model.go @@ -139,9 +139,8 @@ func (s *StatusAndPrompt) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } } - newTextInput, cmd := s.textInput.Update(msg) - s.textInput = newTextInput - return s, cmd + s.textInput = cc.Collect(s.textInput.Update(msg)).(textinput.Model) + return s, cc.Cmd() } else { s.statusMessage = "" } diff --git a/test/cmd/load-test-table/main.go b/test/cmd/load-test-table/main.go index 80b4272..3b730a4 100644 --- a/test/cmd/load-test-table/main.go +++ b/test/cmd/load-test-table/main.go @@ -95,7 +95,6 @@ func main() { for i := 0; i < totalItems; i++ { if err := tableService.Put(ctx, inventoryTableInfo, models.Item{ "pk": &types.AttributeValueMemberS{Value: key}, - "sk": &types.AttributeValueMemberN{Value: fmt.Sprint(i)}, "uuid": &types.AttributeValueMemberS{Value: gofakeit.UUID()}, }); err != nil { log.Fatalln(err)