From b2ddc62555f02432edec6d0886d49494231276ed Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Wed, 1 May 2024 21:30:47 +1000 Subject: [PATCH 01/13] ucl: integrated ucl with the command evaluator --- cmd/dynamo-browse/main.go | 1 + go.mod | 9 +- go.sum | 12 ++ internal/common/ui/commandctrl/commandctrl.go | 61 +++++-- internal/common/ui/commandctrl/types.go | 9 +- internal/dynamo-browse/controllers/scripts.go | 13 +- internal/dynamo-browse/ui/model.go | 163 ++++++++++-------- 7 files changed, 170 insertions(+), 98 deletions(-) diff --git a/cmd/dynamo-browse/main.go b/cmd/dynamo-browse/main.go index 8adcce4..ac7ac1f 100644 --- a/cmd/dynamo-browse/main.go +++ b/cmd/dynamo-browse/main.go @@ -187,6 +187,7 @@ func main() { jobsController.SetMessageSender(p.Send) scriptController.Init() scriptController.SetMessageSender(p.Send) + commandController.SetMessageSender(p.Send) log.Println("launching") if err := p.Start(); err != nil { diff --git a/go.mod b/go.mod index 96398dc..bf5197e 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.22 toolchain go1.22.0 require ( - github.com/alecthomas/participle/v2 v2.0.0-beta.5 + github.com/alecthomas/participle/v2 v2.1.1 github.com/asdine/storm v2.1.2+incompatible github.com/aws/aws-sdk-go-v2 v1.18.1 github.com/aws/aws-sdk-go-v2/config v1.18.27 @@ -23,13 +23,13 @@ require ( github.com/cloudcmds/tamarin v1.0.0 github.com/lmika/events v0.0.0-20200906102219-a2269cd4394e github.com/lmika/go-bubble-table v0.2.2-0.20220616114432-6bbb2995e538 - github.com/lmika/gopkgs v0.0.0-20211210041137-0dc91e939890 + github.com/lmika/gopkgs v0.0.0-20240408110817-a02f6fc67d1f github.com/lmika/shellwords v0.0.0-20140714114018-ce258dd729fe github.com/mattn/go-runewidth v0.0.14 github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70 github.com/muesli/reflow v0.3.0 github.com/pkg/errors v0.9.1 - github.com/stretchr/testify v1.8.4 + github.com/stretchr/testify v1.9.0 golang.design/x/clipboard v0.6.2 golang.org/x/exp v0.0.0-20230108222341-4b8118a2686a ) @@ -101,7 +101,7 @@ require ( github.com/risor-io/risor v1.4.0 // indirect github.com/rivo/uniseg v0.4.2 // indirect github.com/sahilm/fuzzy v0.1.0 // indirect - github.com/stretchr/objx v0.5.0 // indirect + github.com/stretchr/objx v0.5.2 // indirect github.com/tidwall/gjson v1.14.3 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect @@ -117,4 +117,5 @@ require ( golang.org/x/text v0.9.0 // indirect google.golang.org/appengine v1.6.7 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + ucl.lmika.dev v0.0.0-20240501110514-25594c80d273 // indirect ) diff --git a/go.sum b/go.sum index 7d71b0d..526e0ec 100644 --- a/go.sum +++ b/go.sum @@ -7,6 +7,8 @@ github.com/Sereal/Sereal v0.0.0-20220220040404-e0d1e550e879/go.mod h1:D0JMgToj/W github.com/alecthomas/assert/v2 v2.0.3 h1:WKqJODfOiQG0nEJKFKzDIG3E29CN2/4zR9XGJzKIkbg= github.com/alecthomas/participle/v2 v2.0.0-beta.5 h1:y6dsSYVb1G5eK6mgmy+BgI3Mw35a3WghArZ/Hbebrjo= github.com/alecthomas/participle/v2 v2.0.0-beta.5/go.mod h1:RC764t6n4L8D8ITAJv0qdokritYSNR3wV5cVwmIEaMM= +github.com/alecthomas/participle/v2 v2.1.1 h1:hrjKESvSqGHzRb4yW1ciisFJ4p3MGYih6icjJvbsmV8= +github.com/alecthomas/participle/v2 v2.1.1/go.mod h1:Y1+hAs8DHPmc3YUFzqllV+eSQ9ljPTk0ZkPMtEdAx2c= github.com/alecthomas/repr v0.1.0 h1:ENn2e1+J3k09gyj2shc0dHr/yjaWSHRlrJ4DPMevDqE= github.com/anthonynsimon/bild v0.13.0 h1:mN3tMaNds1wBWi1BrJq0ipDBhpkooYfu7ZFSMhXt1C8= github.com/anthonynsimon/bild v0.13.0/go.mod h1:tpzzp0aYkAsMi1zmfhimaDyX1xjn2OUc1AJZK/TF0AE= @@ -223,6 +225,8 @@ github.com/lmika/go-bubble-table v0.2.2-0.20220616114432-6bbb2995e538 h1:dtMPRNo github.com/lmika/go-bubble-table v0.2.2-0.20220616114432-6bbb2995e538/go.mod h1:0RT1upgKZ6qZ6B1SqseE3wWsPjSQRv/G/HjpYK8jNsg= github.com/lmika/gopkgs v0.0.0-20211210041137-0dc91e939890 h1:mwl/exYV/WkBMeShqK7q+B2w2r+b0vP1TSA7clBn9kI= github.com/lmika/gopkgs v0.0.0-20211210041137-0dc91e939890/go.mod h1:FH6OJSvYcJ9xY8CGs9yGgR89kMCK1UimuUQ6kE5YuJQ= +github.com/lmika/gopkgs v0.0.0-20240408110817-a02f6fc67d1f h1:tz68Lhc1oR15HVz69IGbtdukdH0x70kBDEvvj5pTXyE= +github.com/lmika/gopkgs v0.0.0-20240408110817-a02f6fc67d1f/go.mod h1:zHQvhjGXRro/Xp2C9dbC+ZUpE0gL4GYW75x1lk7hwzI= github.com/lmika/shellwords v0.0.0-20140714114018-ce258dd729fe h1:1UXS/6OFkbi6JrihPykmYO1VtsABB02QQ+YmYYzTY18= github.com/lmika/shellwords v0.0.0-20140714114018-ce258dd729fe/go.mod h1:qpdOkLougV5Yry4Px9f1w1pNMavcr6Z67VW5Ro+vW5I= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= @@ -288,6 +292,8 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= @@ -300,6 +306,8 @@ github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gt github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/tidwall/gjson v1.14.3 h1:9jvXn7olKEHU1S9vwoMGliaT8jq1vJ7IH/n9zD9Dnlw= github.com/tidwall/gjson v1.14.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= @@ -418,3 +426,7 @@ gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +ucl.lmika.dev v0.0.0-20240427010304-6315afc54287 h1:llPHrjca54duvQx9PgMTFDhOW2VQiVvqV1CEHpO4AnY= +ucl.lmika.dev v0.0.0-20240427010304-6315afc54287/go.mod h1:T6V4jIUxlWvMTgn4J752VDHNA8iyVrEX6v98EvDj8G4= +ucl.lmika.dev v0.0.0-20240501110514-25594c80d273 h1:+JpKw02VTAcOjJw7Q6juun/9hk9ypNSdTRlf+E4M5Nw= +ucl.lmika.dev v0.0.0-20240501110514-25594c80d273/go.mod h1:T6V4jIUxlWvMTgn4J752VDHNA8iyVrEX6v98EvDj8G4= diff --git a/internal/common/ui/commandctrl/commandctrl.go b/internal/common/ui/commandctrl/commandctrl.go index c0d857f..a1360d5 100644 --- a/internal/common/ui/commandctrl/commandctrl.go +++ b/internal/common/ui/commandctrl/commandctrl.go @@ -10,6 +10,7 @@ import ( "os" "path/filepath" "strings" + "ucl.lmika.dev/ucl" "github.com/lmika/dynamo-browse/internal/common/ui/events" "github.com/lmika/shellwords" @@ -18,18 +19,25 @@ import ( const commandsCategory = "commands" type CommandController struct { + uclInst *ucl.Inst historyProvider IterProvider commandList *CommandList + msgSender func(tea.Msg) lookupExtensions []CommandLookupExtension completionProvider CommandCompletionProvider } func NewCommandController(historyProvider IterProvider) *CommandController { - return &CommandController{ + cc := &CommandController{ historyProvider: historyProvider, commandList: nil, lookupExtensions: nil, } + cc.uclInst = ucl.New( + ucl.WithOut(ucl.LineHandler(cc.printLine)), + ucl.WithMissingBuiltinHandler(cc.cmdInvoker), + ) + return cc } func (c *CommandController) AddCommands(ctx *CommandList) { @@ -37,6 +45,10 @@ func (c *CommandController) AddCommands(ctx *CommandList) { c.commandList = ctx } +func (c *CommandController) SetMessageSender(msg func(tea.Msg)) { + c.msgSender = msg +} + func (c *CommandController) AddCommandLookupExtension(ext CommandLookupExtension) { c.lookupExtensions = append(c.lookupExtensions, ext) } @@ -83,29 +95,25 @@ func (c *CommandController) execute(ctx ExecContext, commandInput string) tea.Ms return nil } - tokens := shellwords.Split(input) - command := c.lookupCommand(tokens[0]) - if command == nil { - return events.Error(errors.New("no such command: " + tokens[0])) + res, err := c.uclInst.Eval(context.Background(), commandInput) + if err != nil { + return events.Error(err) } - return command(ctx, tokens[1:]) + if teaMsg, ok := res.(teaMsgWrapper); ok { + return teaMsg.msg + } + return nil } -func (c *CommandController) Alias(commandName string, aliasArgs []string) Command { - return func(ctx ExecContext, args []string) tea.Msg { +func (c *CommandController) Alias(commandName string) Command { + return func(ctx ExecContext, args ucl.CallArgs) tea.Msg { command := c.lookupCommand(commandName) if command == nil { return events.Error(errors.New("no such command: " + commandName)) } - var allArgs []string - if len(aliasArgs) > 0 { - allArgs = append(append([]string{}, aliasArgs...), args...) - } else { - allArgs = args - } - return command(ctx, allArgs) + return command(ctx, args) } } @@ -160,3 +168,26 @@ func (c *CommandController) executeFile(file []byte, filename string) error { } return scnr.Err() } + +func (c *CommandController) cmdInvoker(ctx context.Context, name string, args ucl.CallArgs) (any, error) { + command := c.lookupCommand(name) + if command == nil { + return nil, errors.New("no such command: " + name) + } + + res := command(ExecContext{}, args) + if errMsg, isErrMsg := res.(events.ErrorMsg); isErrMsg { + return nil, errMsg + } + return teaMsgWrapper{res}, nil +} + +func (c *CommandController) printLine(s string) { + if c.msgSender != nil { + c.msgSender(events.StatusMsg(s)) + } +} + +type teaMsgWrapper struct { + msg tea.Msg +} diff --git a/internal/common/ui/commandctrl/types.go b/internal/common/ui/commandctrl/types.go index 7861e09..cd922ff 100644 --- a/internal/common/ui/commandctrl/types.go +++ b/internal/common/ui/commandctrl/types.go @@ -1,11 +1,14 @@ package commandctrl -import tea "github.com/charmbracelet/bubbletea" +import ( + tea "github.com/charmbracelet/bubbletea" + "ucl.lmika.dev/ucl" +) -type Command func(ctx ExecContext, args []string) tea.Msg +type Command func(ctx ExecContext, args ucl.CallArgs) tea.Msg func NoArgCommand(cmd tea.Cmd) Command { - return func(ctx ExecContext, args []string) tea.Msg { + return func(ctx ExecContext, args ucl.CallArgs) tea.Msg { return cmd() } } diff --git a/internal/dynamo-browse/controllers/scripts.go b/internal/dynamo-browse/controllers/scripts.go index f706ddd..ff2b8d6 100644 --- a/internal/dynamo-browse/controllers/scripts.go +++ b/internal/dynamo-browse/controllers/scripts.go @@ -5,6 +5,7 @@ import ( "fmt" "log" "strings" + "ucl.lmika.dev/ucl" tea "github.com/charmbracelet/bubbletea" "github.com/lmika/dynamo-browse/internal/common/ui/commandctrl" @@ -106,11 +107,19 @@ func (sc *ScriptController) LookupCommand(name string) commandctrl.Command { return nil } - return func(execCtx commandctrl.ExecContext, args []string) tea.Msg { + return func(execCtx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg { errChan := sc.waitAndPrintScriptError() ctx := context.Background() - if err := cmd.Invoke(ctx, args, errChan); err != nil { + invokeArgs := make([]string, 0) + for args.NArgs() > 0 { + var s string + if err := args.Bind(&s); err == nil { + invokeArgs = append(invokeArgs, s) + } + } + + if err := cmd.Invoke(ctx, invokeArgs, errChan); err != nil { return events.Error(err) } return nil diff --git a/internal/dynamo-browse/ui/model.go b/internal/dynamo-browse/ui/model.go index cb07519..8d3297e 100644 --- a/internal/dynamo-browse/ui/model.go +++ b/internal/dynamo-browse/ui/model.go @@ -3,7 +3,7 @@ package ui import ( "log" "os" - "strings" + "ucl.lmika.dev/ucl" "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" @@ -97,30 +97,32 @@ func NewModel( cc.AddCommands(&commandctrl.CommandList{ Commands: map[string]commandctrl.Command{ "quit": commandctrl.NoArgCommand(tea.Quit), - "table": func(ctx commandctrl.ExecContext, args []string) tea.Msg { - if len(args) == 0 { - return rc.ListTables(false) - } else { - return rc.ScanTable(args[0]) + "table": func(ctx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg { + var tableName string + if err := args.Bind(&tableName); err == nil { + return rc.ScanTable(tableName) } + + return rc.ListTables(false) }, - "export": func(ctx commandctrl.ExecContext, args []string) tea.Msg { - if len(args) == 0 { + "export": func(ctx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg { + var filename string + if err := args.Bind(&filename); err != nil { return events.Error(errors.New("expected filename")) } - opts := controllers.ExportOptions{} - if len(args) == 2 && args[0] == "-all" { - opts.AllResults = true - args = args[1:] + opts := controllers.ExportOptions{ + AllResults: args.HasSwitch("all"), } - return exportController.ExportCSV(args[0], opts) + return exportController.ExportCSV(filename, opts) }, - "mark": func(ctx commandctrl.ExecContext, args []string) tea.Msg { + "mark": func(ctx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg { var markOp = controllers.MarkOpMark - if len(args) > 0 { - switch args[0] { + + var markOpStr string + if err := args.Bind(&markOpStr); err == nil { + switch markOpStr { case "all": markOp = controllers.MarkOpMark case "none": @@ -133,108 +135,121 @@ func NewModel( } var whereExpr = "" - if len(args) == 3 && args[1] == "-where" { - whereExpr = args[2] - } + _ = args.BindSwitch("where", &whereExpr) return rc.Mark(markOp, whereExpr) }, - "next-page": func(ctx commandctrl.ExecContext, args []string) tea.Msg { + "unmark": func(ctx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg { + return rc.Mark(controllers.MarkOpUnmark, "") + }, + "next-page": func(ctx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg { return rc.NextPage() }, "delete": commandctrl.NoArgCommand(wc.DeleteMarked), // TEMP "new-item": commandctrl.NoArgCommand(wc.NewItem), - "clone": func(ctx commandctrl.ExecContext, args []string) tea.Msg { + "clone": func(ctx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg { return wc.CloneItem(dtv.SelectedItemIndex()) }, - "set-attr": func(ctx commandctrl.ExecContext, args []string) tea.Msg { - if len(args) == 0 { + "set-attr": func(ctx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg { + var fieldName string + if err := args.Bind(&fieldName); err != nil { return events.Error(errors.New("expected field")) } var itemType = models.UnsetItemType - if len(args) == 2 { - switch strings.ToUpper(args[0]) { - case "-S": - itemType = models.StringItemType - case "-N": - itemType = models.NumberItemType - case "-BOOL": - itemType = models.BoolItemType - case "-NULL": - itemType = models.NullItemType - case "-TO": - itemType = models.ExprValueItemType - default: - return events.Error(errors.New("unrecognised item type")) - } - args = args[1:] + switch { + case args.HasSwitch("S"): + itemType = models.StringItemType + case args.HasSwitch("N"): + itemType = models.NumberItemType + case args.HasSwitch("BOOL"): + itemType = models.BoolItemType + case args.HasSwitch("NULL"): + itemType = models.NullItemType + case args.HasSwitch("TO"): + itemType = models.ExprValueItemType + default: + return events.Error(errors.New("unrecognised item type")) } - return wc.SetAttributeValue(dtv.SelectedItemIndex(), itemType, args[0]) + return wc.SetAttributeValue(dtv.SelectedItemIndex(), itemType, fieldName) }, - "del-attr": func(ctx commandctrl.ExecContext, args []string) tea.Msg { - if len(args) == 0 { + "del-attr": func(ctx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg { + var fieldName string + // TODO: support rest args + if err := args.Bind(&fieldName); err != nil { return events.Error(errors.New("expected field")) } - return wc.DeleteAttribute(dtv.SelectedItemIndex(), args[0]) + + return wc.DeleteAttribute(dtv.SelectedItemIndex(), fieldName) }, - "put": func(ctx commandctrl.ExecContext, args []string) tea.Msg { + "put": func(ctx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg { return wc.PutItems() }, - "touch": func(ctx commandctrl.ExecContext, args []string) tea.Msg { + "touch": func(ctx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg { return wc.TouchItem(dtv.SelectedItemIndex()) }, - "noisy-touch": func(ctx commandctrl.ExecContext, args []string) tea.Msg { + "noisy-touch": func(ctx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg { return wc.NoisyTouchItem(dtv.SelectedItemIndex()) }, - "echo": func(ctx commandctrl.ExecContext, args []string) tea.Msg { - s := new(strings.Builder) - for _, arg := range args { - s.WriteString(arg) + /* + "echo": func(ctx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg { + s := new(strings.Builder) + for _, arg := range args { + s.WriteString(arg) + } + return events.StatusMsg(s.String()) + }, + */ + "set-opt": func(ctx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg { + var name string + if err := args.Bind(&name); err != nil { + return events.Error(errors.New("expected settingName")) } - return events.StatusMsg(s.String()) - }, - "set": func(ctx commandctrl.ExecContext, args []string) tea.Msg { - switch len(args) { - case 1: - return settingsController.SetSetting(args[0], "") - case 2: - return settingsController.SetSetting(args[0], args[1]) + + var value string + if err := args.Bind(&value); err == nil { + return settingsController.SetSetting(name, value) } - return events.Error(errors.New("expected: settingName [value]")) + + return settingsController.SetSetting(name, "") }, - "rebind": func(ctx commandctrl.ExecContext, args []string) tea.Msg { - if len(args) != 2 { + "rebind": func(ctx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg { + var bindingName, newKey string + if err := args.Bind(&bindingName, &newKey); err != nil { return events.Error(errors.New("expected: bindingName newKey")) } - return keyBindingController.Rebind(args[0], args[1], ctx.FromFile) + + return keyBindingController.Rebind(bindingName, newKey, ctx.FromFile) }, - "run-script": func(ctx commandctrl.ExecContext, args []string) tea.Msg { - if len(args) != 1 { + "run-script": func(ctx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg { + var name string + if err := args.Bind(&name); err != nil { return events.Error(errors.New("expected: script name")) } - return scriptController.RunScript(args[0]) + + return scriptController.RunScript(name) }, - "load-script": func(ctx commandctrl.ExecContext, args []string) tea.Msg { - if len(args) != 1 { + "load-script": func(ctx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg { + var name string + if err := args.Bind(&name); err != nil { return events.Error(errors.New("expected: script name")) } - return scriptController.LoadScript(args[0]) + + return scriptController.LoadScript(name) }, // Aliases - "unmark": cc.Alias("mark", []string{"none"}), - "sa": cc.Alias("set-attr", nil), - "da": cc.Alias("del-attr", nil), - "np": cc.Alias("next-page", nil), - "w": cc.Alias("put", nil), - "q": cc.Alias("quit", nil), + "sa": cc.Alias("set-attr"), + "da": cc.Alias("del-attr"), + "np": cc.Alias("next-page"), + "w": cc.Alias("put"), + "q": cc.Alias("quit"), }, }) From 29d425c77eeb2a6d0120f92882b546762ae04df1 Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Wed, 1 May 2024 21:45:47 +1000 Subject: [PATCH 02/13] Fixed deadlock with message listener --- cmd/dynamo-browse/main.go | 2 +- internal/common/ui/commandctrl/commandctrl.go | 15 ++++++++++----- internal/dynamo-browse/ui/model.go | 2 -- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/cmd/dynamo-browse/main.go b/cmd/dynamo-browse/main.go index ac7ac1f..3a89f66 100644 --- a/cmd/dynamo-browse/main.go +++ b/cmd/dynamo-browse/main.go @@ -187,7 +187,7 @@ func main() { jobsController.SetMessageSender(p.Send) scriptController.Init() scriptController.SetMessageSender(p.Send) - commandController.SetMessageSender(p.Send) + go commandController.StartMessageSender(p.Send) log.Println("launching") if err := p.Start(); err != nil { diff --git a/internal/common/ui/commandctrl/commandctrl.go b/internal/common/ui/commandctrl/commandctrl.go index a1360d5..206f8a6 100644 --- a/internal/common/ui/commandctrl/commandctrl.go +++ b/internal/common/ui/commandctrl/commandctrl.go @@ -22,9 +22,9 @@ type CommandController struct { uclInst *ucl.Inst historyProvider IterProvider commandList *CommandList - msgSender func(tea.Msg) lookupExtensions []CommandLookupExtension completionProvider CommandCompletionProvider + msgChan chan tea.Msg } func NewCommandController(historyProvider IterProvider) *CommandController { @@ -32,6 +32,7 @@ func NewCommandController(historyProvider IterProvider) *CommandController { historyProvider: historyProvider, commandList: nil, lookupExtensions: nil, + msgChan: make(chan tea.Msg), } cc.uclInst = ucl.New( ucl.WithOut(ucl.LineHandler(cc.printLine)), @@ -45,8 +46,10 @@ func (c *CommandController) AddCommands(ctx *CommandList) { c.commandList = ctx } -func (c *CommandController) SetMessageSender(msg func(tea.Msg)) { - c.msgSender = msg +func (c *CommandController) StartMessageSender(msgSender func(tea.Msg)) { + for msg := range c.msgChan { + msgSender(msg) + } } func (c *CommandController) AddCommandLookupExtension(ext CommandLookupExtension) { @@ -183,8 +186,10 @@ func (c *CommandController) cmdInvoker(ctx context.Context, name string, args uc } func (c *CommandController) printLine(s string) { - if c.msgSender != nil { - c.msgSender(events.StatusMsg(s)) + select { + case c.msgChan <- events.StatusMsg(s): + default: + log.Println(s) } } diff --git a/internal/dynamo-browse/ui/model.go b/internal/dynamo-browse/ui/model.go index 8d3297e..ac6d873 100644 --- a/internal/dynamo-browse/ui/model.go +++ b/internal/dynamo-browse/ui/model.go @@ -170,8 +170,6 @@ func NewModel( itemType = models.NullItemType case args.HasSwitch("TO"): itemType = models.ExprValueItemType - default: - return events.Error(errors.New("unrecognised item type")) } return wc.SetAttributeValue(dtv.SelectedItemIndex(), itemType, fieldName) From 94b58e2168be49bbec4f6dd015099b0ae3f0d046 Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Sat, 4 May 2024 11:40:24 +1000 Subject: [PATCH 03/13] Updated UCL and added an interactive mode --- go.mod | 2 +- go.sum | 6 +++ internal/common/ui/commandctrl/commandctrl.go | 45 ++++++++++--------- 3 files changed, 30 insertions(+), 23 deletions(-) diff --git a/go.mod b/go.mod index bf5197e..2eb4b60 100644 --- a/go.mod +++ b/go.mod @@ -117,5 +117,5 @@ require ( golang.org/x/text v0.9.0 // indirect google.golang.org/appengine v1.6.7 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - ucl.lmika.dev v0.0.0-20240501110514-25594c80d273 // indirect + ucl.lmika.dev v0.0.0-20240504013531-0dc9fd3c3281 // indirect ) diff --git a/go.sum b/go.sum index 526e0ec..2286e36 100644 --- a/go.sum +++ b/go.sum @@ -5,6 +5,8 @@ github.com/DataDog/zstd v1.5.2/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwS github.com/Sereal/Sereal v0.0.0-20220220040404-e0d1e550e879 h1:M5ptEKnqKqpFTKbe+p5zEf3ro1deJ6opUz5j3g3/ErQ= github.com/Sereal/Sereal v0.0.0-20220220040404-e0d1e550e879/go.mod h1:D0JMgToj/WdxCgd30Kc1UcA9E+WdZoJqeVOuYW7iTBM= github.com/alecthomas/assert/v2 v2.0.3 h1:WKqJODfOiQG0nEJKFKzDIG3E29CN2/4zR9XGJzKIkbg= +github.com/alecthomas/participle v0.7.1 h1:2bN7reTw//5f0cugJcTOnY/NYZcWQOaajW+BwZB5xWs= +github.com/alecthomas/participle v0.7.1/go.mod h1:HfdmEuwvr12HXQN44HPWXR0lHmVolVYe4dyL6lQ3duY= github.com/alecthomas/participle/v2 v2.0.0-beta.5 h1:y6dsSYVb1G5eK6mgmy+BgI3Mw35a3WghArZ/Hbebrjo= github.com/alecthomas/participle/v2 v2.0.0-beta.5/go.mod h1:RC764t6n4L8D8ITAJv0qdokritYSNR3wV5cVwmIEaMM= github.com/alecthomas/participle/v2 v2.1.1 h1:hrjKESvSqGHzRb4yW1ciisFJ4p3MGYih6icjJvbsmV8= @@ -430,3 +432,7 @@ ucl.lmika.dev v0.0.0-20240427010304-6315afc54287 h1:llPHrjca54duvQx9PgMTFDhOW2VQ ucl.lmika.dev v0.0.0-20240427010304-6315afc54287/go.mod h1:T6V4jIUxlWvMTgn4J752VDHNA8iyVrEX6v98EvDj8G4= ucl.lmika.dev v0.0.0-20240501110514-25594c80d273 h1:+JpKw02VTAcOjJw7Q6juun/9hk9ypNSdTRlf+E4M5Nw= ucl.lmika.dev v0.0.0-20240501110514-25594c80d273/go.mod h1:T6V4jIUxlWvMTgn4J752VDHNA8iyVrEX6v98EvDj8G4= +ucl.lmika.dev v0.0.0-20240504001444-cf3a12bf0d4d h1:OqGmR0Y+OG6aFIOlXy2QwEHtuUNasYCh/6cxHokYQj4= +ucl.lmika.dev v0.0.0-20240504001444-cf3a12bf0d4d/go.mod h1:T6V4jIUxlWvMTgn4J752VDHNA8iyVrEX6v98EvDj8G4= +ucl.lmika.dev v0.0.0-20240504013531-0dc9fd3c3281 h1:/M7phiv/0XVp3wKkOxEnGQysf8+RS6NOaBQZyUEoSsA= +ucl.lmika.dev v0.0.0-20240504013531-0dc9fd3c3281/go.mod h1:T6V4jIUxlWvMTgn4J752VDHNA8iyVrEX6v98EvDj8G4= diff --git a/internal/common/ui/commandctrl/commandctrl.go b/internal/common/ui/commandctrl/commandctrl.go index 206f8a6..988021e 100644 --- a/internal/common/ui/commandctrl/commandctrl.go +++ b/internal/common/ui/commandctrl/commandctrl.go @@ -1,8 +1,6 @@ package commandctrl import ( - "bufio" - "bytes" "context" tea "github.com/charmbracelet/bubbletea" "github.com/pkg/errors" @@ -11,6 +9,7 @@ import ( "path/filepath" "strings" "ucl.lmika.dev/ucl" + "ucl.lmika.dev/ucl/builtins" "github.com/lmika/dynamo-browse/internal/common/ui/events" "github.com/lmika/shellwords" @@ -25,6 +24,7 @@ type CommandController struct { lookupExtensions []CommandLookupExtension completionProvider CommandCompletionProvider msgChan chan tea.Msg + interactive bool } func NewCommandController(historyProvider IterProvider) *CommandController { @@ -33,10 +33,13 @@ func NewCommandController(historyProvider IterProvider) *CommandController { commandList: nil, lookupExtensions: nil, msgChan: make(chan tea.Msg), + interactive: true, } cc.uclInst = ucl.New( ucl.WithOut(ucl.LineHandler(cc.printLine)), ucl.WithMissingBuiltinHandler(cc.cmdInvoker), + ucl.WithModule(builtins.OS()), + ucl.WithModule(builtins.FS(nil)), ) return cc } @@ -136,6 +139,12 @@ func (c *CommandController) lookupCommand(name string) Command { } func (c *CommandController) ExecuteFile(filename string) error { + oldInteractive := c.interactive + c.interactive = false + defer func() { + c.interactive = oldInteractive + }() + baseFilename := filepath.Base(filename) if rcFile, err := os.ReadFile(filename); err == nil { @@ -149,27 +158,14 @@ func (c *CommandController) ExecuteFile(filename string) error { } func (c *CommandController) executeFile(file []byte, filename string) error { - scnr := bufio.NewScanner(bytes.NewReader(file)) - - lineNo := 0 - for scnr.Scan() { - lineNo++ - line := strings.TrimSpace(scnr.Text()) - if line == "" { - continue - } else if line[0] == '#' { - continue - } - - msg := c.execute(ExecContext{FromFile: true}, line) - switch m := msg.(type) { - case events.ErrorMsg: - log.Printf("%v:%v: error - %v", filename, lineNo, m.Error()) - case events.StatusMsg: - log.Printf("%v:%v: %v", filename, lineNo, string(m)) - } + msg := c.execute(ExecContext{FromFile: true}, string(file)) + switch m := msg.(type) { + case events.ErrorMsg: + log.Printf("%v: error - %v", filename, m.Error()) + case events.StatusMsg: + log.Printf("%v: %v", filename, string(m)) } - return scnr.Err() + return nil } func (c *CommandController) cmdInvoker(ctx context.Context, name string, args ucl.CallArgs) (any, error) { @@ -186,6 +182,11 @@ func (c *CommandController) cmdInvoker(ctx context.Context, name string, args uc } func (c *CommandController) printLine(s string) { + if c.msgChan == nil || !c.interactive { + log.Println(s) + return + } + select { case c.msgChan <- events.StatusMsg(s): default: From 17381f3d0b841987e2db355d638690b29f52f9e9 Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Thu, 15 May 2025 22:16:02 +1000 Subject: [PATCH 04/13] Started re-engineering the UCL command instance --- .gitignore | 2 + cmd/dynamo-browse/main.go | 7 +- go.mod | 2 +- go.sum | 2 + .../common/ui/commandctrl/cmdpacks/stdcmds.go | 60 ++++ internal/common/ui/commandctrl/commandctrl.go | 76 ++++- internal/common/ui/commandctrl/ctx.go | 17 + internal/common/ui/commandctrl/packs.go | 7 + internal/dynamo-browse/ui/model.go | 308 +++++++++--------- 9 files changed, 309 insertions(+), 172 deletions(-) create mode 100644 internal/common/ui/commandctrl/cmdpacks/stdcmds.go create mode 100644 internal/common/ui/commandctrl/ctx.go create mode 100644 internal/common/ui/commandctrl/packs.go diff --git a/.gitignore b/.gitignore index b14c548..2b59837 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ debug.log +.DS_store +.idea diff --git a/cmd/dynamo-browse/main.go b/cmd/dynamo-browse/main.go index 3a89f66..7b11624 100644 --- a/cmd/dynamo-browse/main.go +++ b/cmd/dynamo-browse/main.go @@ -4,6 +4,7 @@ import ( "context" "flag" "fmt" + "github.com/lmika/dynamo-browse/internal/common/ui/commandctrl/cmdpacks" "log" "net" "os" @@ -159,7 +160,11 @@ func main() { keyBindingService := keybindings_service.NewService(keyBindings) keyBindingController := controllers.NewKeyBindingController(keyBindingService, scriptController) - commandController := commandctrl.NewCommandController(inputHistoryService) + commandController := commandctrl.NewCommandController(inputHistoryService, + cmdpacks.StandardCommands{ + ReadController: tableReadController, + }, + ) commandController.AddCommandLookupExtension(scriptController) commandController.SetCommandCompletionProvider(columnsController) diff --git a/go.mod b/go.mod index 2eb4b60..4f87cec 100644 --- a/go.mod +++ b/go.mod @@ -117,5 +117,5 @@ require ( golang.org/x/text v0.9.0 // indirect google.golang.org/appengine v1.6.7 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - ucl.lmika.dev v0.0.0-20240504013531-0dc9fd3c3281 // indirect + ucl.lmika.dev v0.0.0-20250306030053-ad6d002a22e8 // indirect ) diff --git a/go.sum b/go.sum index 2286e36..f26fcfa 100644 --- a/go.sum +++ b/go.sum @@ -436,3 +436,5 @@ ucl.lmika.dev v0.0.0-20240504001444-cf3a12bf0d4d h1:OqGmR0Y+OG6aFIOlXy2QwEHtuUNa ucl.lmika.dev v0.0.0-20240504001444-cf3a12bf0d4d/go.mod h1:T6V4jIUxlWvMTgn4J752VDHNA8iyVrEX6v98EvDj8G4= ucl.lmika.dev v0.0.0-20240504013531-0dc9fd3c3281 h1:/M7phiv/0XVp3wKkOxEnGQysf8+RS6NOaBQZyUEoSsA= ucl.lmika.dev v0.0.0-20240504013531-0dc9fd3c3281/go.mod h1:T6V4jIUxlWvMTgn4J752VDHNA8iyVrEX6v98EvDj8G4= +ucl.lmika.dev v0.0.0-20250306030053-ad6d002a22e8 h1:vWttdW8GJWcTUQeJFbQHqCHJDLFWQ9nccUTx/lW2v8s= +ucl.lmika.dev v0.0.0-20250306030053-ad6d002a22e8/go.mod h1:FMP2ncSu4UxfvB0iA2zlebwL+1UPCARdyYNOrmi86A4= diff --git a/internal/common/ui/commandctrl/cmdpacks/stdcmds.go b/internal/common/ui/commandctrl/cmdpacks/stdcmds.go new file mode 100644 index 0000000..610d750 --- /dev/null +++ b/internal/common/ui/commandctrl/cmdpacks/stdcmds.go @@ -0,0 +1,60 @@ +package cmdpacks + +import ( + "context" + tea "github.com/charmbracelet/bubbletea" + "github.com/lmika/dynamo-browse/internal/common/ui/commandctrl" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/controllers" + "ucl.lmika.dev/repl" + "ucl.lmika.dev/ucl" +) + +type StandardCommands struct { + ReadController *controllers.TableReadController +} + +var cmdQuitDoc = repl.Doc{ + Brief: "Quits dynamo-browse", + Usage: "quit", + Detailed: ` + This will quit dynamo-browse immediately, without prompting to apply + any changes. + `, +} + +func (sc StandardCommands) cmdQuit(ctx context.Context, args ucl.CallArgs) (any, error) { + commandctrl.PostMsg(ctx, tea.Quit) + return nil, nil +} + +var cmdTableDoc = repl.Doc{ + Brief: "Prompt for table to scan", + Usage: "table [NAME]", + Args: []repl.ArgDoc{ + {Name: "name", Brief: "Name of the table to scan"}, + }, + Detailed: ` + If called with an argument, it will scan the table with that name and + replace the current result set. If called without an argument, it will + prompt for a table to scan. + + This command is intended only for interactive sessions and is not suitable + for scripting. The scan or table prompts will happen asynchronously. + `, +} + +func (sc StandardCommands) cmdTable(ctx context.Context, args ucl.CallArgs) (any, error) { + var tableName string + if err := args.Bind(&tableName); err == nil { + commandctrl.PostMsg(ctx, sc.ReadController.ScanTable(tableName)) + return nil, nil + } + + commandctrl.PostMsg(ctx, sc.ReadController.ListTables(false)) + return nil, nil +} + +func (sc StandardCommands) ConfigureUCL(ucl *ucl.Inst) { + ucl.SetBuiltin("quit", sc.cmdQuit) + ucl.SetBuiltin("table", sc.cmdTable) +} diff --git a/internal/common/ui/commandctrl/commandctrl.go b/internal/common/ui/commandctrl/commandctrl.go index 988021e..8c110a4 100644 --- a/internal/common/ui/commandctrl/commandctrl.go +++ b/internal/common/ui/commandctrl/commandctrl.go @@ -2,6 +2,7 @@ package commandctrl import ( "context" + "fmt" tea "github.com/charmbracelet/bubbletea" "github.com/pkg/errors" "log" @@ -17,30 +18,42 @@ import ( const commandsCategory = "commands" +type cmdMessage struct { + cmd string +} + type CommandController struct { uclInst *ucl.Inst historyProvider IterProvider commandList *CommandList lookupExtensions []CommandLookupExtension completionProvider CommandCompletionProvider + cmdChan chan cmdMessage msgChan chan tea.Msg interactive bool } -func NewCommandController(historyProvider IterProvider) *CommandController { +func NewCommandController(historyProvider IterProvider, pkgs ...CommandPack) *CommandController { cc := &CommandController{ historyProvider: historyProvider, commandList: nil, lookupExtensions: nil, + cmdChan: make(chan cmdMessage), msgChan: make(chan tea.Msg), interactive: true, } cc.uclInst = ucl.New( ucl.WithOut(ucl.LineHandler(cc.printLine)), - ucl.WithMissingBuiltinHandler(cc.cmdInvoker), ucl.WithModule(builtins.OS()), ucl.WithModule(builtins.FS(nil)), ) + + for _, pkg := range pkgs { + pkg.ConfigureUCL(cc.uclInst) + } + + go cc.cmdLooper() + return cc } @@ -101,17 +114,42 @@ func (c *CommandController) execute(ctx ExecContext, commandInput string) tea.Ms return nil } - res, err := c.uclInst.Eval(context.Background(), commandInput) - if err != nil { - return events.Error(err) + select { + case c.cmdChan <- cmdMessage{cmd: input}: + // good + default: + return events.Error(errors.New("command currently running")) } - if teaMsg, ok := res.(teaMsgWrapper); ok { - return teaMsg.msg - } + /* + res, err := c.uclInst.Eval(context.Background(), commandInput) + if err != nil { + return events.Error(err) + } + + if teaMsg, ok := res.(teaMsgWrapper); ok { + return teaMsg.msg + } + */ return nil } +func (c *CommandController) cmdLooper() { + ctx := context.WithValue(context.Background(), commandCtlKey, c) + + for { + select { + case cmdChan := <-c.cmdChan: + res, err := c.uclInst.Eval(ctx, cmdChan.cmd) + if err != nil { + c.postMessage(events.Error(err)) + } else if res != nil { + c.postMessage(events.StatusMsg(fmt.Sprint(res))) + } + } + } +} + func (c *CommandController) Alias(commandName string) Command { return func(ctx ExecContext, args ucl.CallArgs) tea.Msg { command := c.lookupCommand(commandName) @@ -158,13 +196,13 @@ func (c *CommandController) ExecuteFile(filename string) error { } func (c *CommandController) executeFile(file []byte, filename string) error { - msg := c.execute(ExecContext{FromFile: true}, string(file)) - switch m := msg.(type) { - case events.ErrorMsg: - log.Printf("%v: error - %v", filename, m.Error()) - case events.StatusMsg: - log.Printf("%v: %v", filename, string(m)) - } + //msg := c.execute(ExecContext{FromFile: true}, string(file)) + //switch m := msg.(type) { + //case events.ErrorMsg: + // log.Printf("%v: error - %v", filename, m.Error()) + //case events.StatusMsg: + // log.Printf("%v: %v", filename, string(m)) + //} return nil } @@ -194,6 +232,14 @@ func (c *CommandController) printLine(s string) { } } +func (c *CommandController) postMessage(msg tea.Msg) { + if c.msgChan == nil { + return + } + + c.msgChan <- msg +} + type teaMsgWrapper struct { msg tea.Msg } diff --git a/internal/common/ui/commandctrl/ctx.go b/internal/common/ui/commandctrl/ctx.go new file mode 100644 index 0000000..a041e33 --- /dev/null +++ b/internal/common/ui/commandctrl/ctx.go @@ -0,0 +1,17 @@ +package commandctrl + +import ( + "context" + tea "github.com/charmbracelet/bubbletea" +) + +type commandCtlKeyType struct{} + +var commandCtlKey = commandCtlKeyType{} + +func PostMsg(ctx context.Context, msg tea.Msg) { + cmdCtl, ok := ctx.Value(commandCtlKey).(*CommandController) + if ok { + cmdCtl.postMessage(msg) + } +} diff --git a/internal/common/ui/commandctrl/packs.go b/internal/common/ui/commandctrl/packs.go new file mode 100644 index 0000000..76d0bd7 --- /dev/null +++ b/internal/common/ui/commandctrl/packs.go @@ -0,0 +1,7 @@ +package commandctrl + +import "ucl.lmika.dev/ucl" + +type CommandPack interface { + ConfigureUCL(ucl *ucl.Inst) +} diff --git a/internal/dynamo-browse/ui/model.go b/internal/dynamo-browse/ui/model.go index ac6d873..d6c0a8e 100644 --- a/internal/dynamo-browse/ui/model.go +++ b/internal/dynamo-browse/ui/model.go @@ -1,16 +1,11 @@ package ui import ( - "log" - "os" - "ucl.lmika.dev/ucl" - "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" "github.com/lmika/dynamo-browse/internal/common/ui/commandctrl" "github.com/lmika/dynamo-browse/internal/common/ui/events" "github.com/lmika/dynamo-browse/internal/dynamo-browse/controllers" - "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" "github.com/lmika/dynamo-browse/internal/dynamo-browse/services" "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/itemrenderer" "github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/keybindings" @@ -26,7 +21,8 @@ import ( "github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/tableselect" "github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/utils" bus "github.com/lmika/events" - "github.com/pkg/errors" + "log" + "os" ) const ( @@ -94,162 +90,164 @@ func NewModel( dialogPrompt := dialogprompt.New(statusAndPrompt) tableSelect := tableselect.New(dialogPrompt, uiStyles) - cc.AddCommands(&commandctrl.CommandList{ - Commands: map[string]commandctrl.Command{ - "quit": commandctrl.NoArgCommand(tea.Quit), - "table": func(ctx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg { - var tableName string - if err := args.Bind(&tableName); err == nil { - return rc.ScanTable(tableName) - } - - return rc.ListTables(false) - }, - "export": func(ctx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg { - var filename string - if err := args.Bind(&filename); err != nil { - return events.Error(errors.New("expected filename")) - } - - opts := controllers.ExportOptions{ - AllResults: args.HasSwitch("all"), - } - - return exportController.ExportCSV(filename, opts) - }, - "mark": func(ctx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg { - var markOp = controllers.MarkOpMark - - var markOpStr string - if err := args.Bind(&markOpStr); err == nil { - switch markOpStr { - case "all": - markOp = controllers.MarkOpMark - case "none": - markOp = controllers.MarkOpUnmark - case "toggle": - markOp = controllers.MarkOpToggle - default: - return events.Error(errors.New("unrecognised mark operation")) + /* + cc.AddCommands(&commandctrl.CommandList{ + Commands: map[string]commandctrl.Command{ + "quit": commandctrl.NoArgCommand(tea.Quit), + "table": func(ctx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg { + var tableName string + if err := args.Bind(&tableName); err == nil { + return rc.ScanTable(tableName) } - } - var whereExpr = "" - _ = args.BindSwitch("where", &whereExpr) - - return rc.Mark(markOp, whereExpr) - }, - "unmark": func(ctx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg { - return rc.Mark(controllers.MarkOpUnmark, "") - }, - "next-page": func(ctx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg { - return rc.NextPage() - }, - "delete": commandctrl.NoArgCommand(wc.DeleteMarked), - - // TEMP - "new-item": commandctrl.NoArgCommand(wc.NewItem), - "clone": func(ctx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg { - return wc.CloneItem(dtv.SelectedItemIndex()) - }, - "set-attr": func(ctx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg { - var fieldName string - if err := args.Bind(&fieldName); err != nil { - return events.Error(errors.New("expected field")) - } - - var itemType = models.UnsetItemType - switch { - case args.HasSwitch("S"): - itemType = models.StringItemType - case args.HasSwitch("N"): - itemType = models.NumberItemType - case args.HasSwitch("BOOL"): - itemType = models.BoolItemType - case args.HasSwitch("NULL"): - itemType = models.NullItemType - case args.HasSwitch("TO"): - itemType = models.ExprValueItemType - } - - return wc.SetAttributeValue(dtv.SelectedItemIndex(), itemType, fieldName) - }, - "del-attr": func(ctx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg { - var fieldName string - // TODO: support rest args - if err := args.Bind(&fieldName); err != nil { - return events.Error(errors.New("expected field")) - } - - return wc.DeleteAttribute(dtv.SelectedItemIndex(), fieldName) - }, - - "put": func(ctx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg { - return wc.PutItems() - }, - "touch": func(ctx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg { - return wc.TouchItem(dtv.SelectedItemIndex()) - }, - "noisy-touch": func(ctx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg { - return wc.NoisyTouchItem(dtv.SelectedItemIndex()) - }, - - /* - "echo": func(ctx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg { - s := new(strings.Builder) - for _, arg := range args { - s.WriteString(arg) - } - return events.StatusMsg(s.String()) + return rc.ListTables(false) }, - */ - "set-opt": func(ctx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg { - var name string - if err := args.Bind(&name); err != nil { - return events.Error(errors.New("expected settingName")) - } + "export": func(ctx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg { + var filename string + if err := args.Bind(&filename); err != nil { + return events.Error(errors.New("expected filename")) + } - var value string - if err := args.Bind(&value); err == nil { - return settingsController.SetSetting(name, value) - } + opts := controllers.ExportOptions{ + AllResults: args.HasSwitch("all"), + } - return settingsController.SetSetting(name, "") + return exportController.ExportCSV(filename, opts) + }, + "mark": func(ctx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg { + var markOp = controllers.MarkOpMark + + var markOpStr string + if err := args.Bind(&markOpStr); err == nil { + switch markOpStr { + case "all": + markOp = controllers.MarkOpMark + case "none": + markOp = controllers.MarkOpUnmark + case "toggle": + markOp = controllers.MarkOpToggle + default: + return events.Error(errors.New("unrecognised mark operation")) + } + } + + var whereExpr = "" + _ = args.BindSwitch("where", &whereExpr) + + return rc.Mark(markOp, whereExpr) + }, + "unmark": func(ctx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg { + return rc.Mark(controllers.MarkOpUnmark, "") + }, + "next-page": func(ctx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg { + return rc.NextPage() + }, + "delete": commandctrl.NoArgCommand(wc.DeleteMarked), + + // TEMP + "new-item": commandctrl.NoArgCommand(wc.NewItem), + "clone": func(ctx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg { + return wc.CloneItem(dtv.SelectedItemIndex()) + }, + "set-attr": func(ctx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg { + var fieldName string + if err := args.Bind(&fieldName); err != nil { + return events.Error(errors.New("expected field")) + } + + var itemType = models.UnsetItemType + switch { + case args.HasSwitch("S"): + itemType = models.StringItemType + case args.HasSwitch("N"): + itemType = models.NumberItemType + case args.HasSwitch("BOOL"): + itemType = models.BoolItemType + case args.HasSwitch("NULL"): + itemType = models.NullItemType + case args.HasSwitch("TO"): + itemType = models.ExprValueItemType + } + + return wc.SetAttributeValue(dtv.SelectedItemIndex(), itemType, fieldName) + }, + "del-attr": func(ctx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg { + var fieldName string + // TODO: support rest args + if err := args.Bind(&fieldName); err != nil { + return events.Error(errors.New("expected field")) + } + + return wc.DeleteAttribute(dtv.SelectedItemIndex(), fieldName) + }, + + "put": func(ctx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg { + return wc.PutItems() + }, + "touch": func(ctx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg { + return wc.TouchItem(dtv.SelectedItemIndex()) + }, + "noisy-touch": func(ctx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg { + return wc.NoisyTouchItem(dtv.SelectedItemIndex()) + }, + + + "echo": func(ctx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg { + s := new(strings.Builder) + for _, arg := range args { + s.WriteString(arg) + } + return events.StatusMsg(s.String()) + }, + "set-opt": func(ctx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg { + var name string + if err := args.Bind(&name); err != nil { + return events.Error(errors.New("expected settingName")) + } + + var value string + if err := args.Bind(&value); err == nil { + return settingsController.SetSetting(name, value) + } + + return settingsController.SetSetting(name, "") + }, + "rebind": func(ctx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg { + var bindingName, newKey string + if err := args.Bind(&bindingName, &newKey); err != nil { + return events.Error(errors.New("expected: bindingName newKey")) + } + + return keyBindingController.Rebind(bindingName, newKey, ctx.FromFile) + }, + + "run-script": func(ctx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg { + var name string + if err := args.Bind(&name); err != nil { + return events.Error(errors.New("expected: script name")) + } + + return scriptController.RunScript(name) + }, + "load-script": func(ctx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg { + var name string + if err := args.Bind(&name); err != nil { + return events.Error(errors.New("expected: script name")) + } + + return scriptController.LoadScript(name) + }, + + // Aliases + "sa": cc.Alias("set-attr"), + "da": cc.Alias("del-attr"), + "np": cc.Alias("next-page"), + "w": cc.Alias("put"), + "q": cc.Alias("quit"), }, - "rebind": func(ctx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg { - var bindingName, newKey string - if err := args.Bind(&bindingName, &newKey); err != nil { - return events.Error(errors.New("expected: bindingName newKey")) - } + }) - return keyBindingController.Rebind(bindingName, newKey, ctx.FromFile) - }, - - "run-script": func(ctx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg { - var name string - if err := args.Bind(&name); err != nil { - return events.Error(errors.New("expected: script name")) - } - - return scriptController.RunScript(name) - }, - "load-script": func(ctx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg { - var name string - if err := args.Bind(&name); err != nil { - return events.Error(errors.New("expected: script name")) - } - - return scriptController.LoadScript(name) - }, - - // Aliases - "sa": cc.Alias("set-attr"), - "da": cc.Alias("del-attr"), - "np": cc.Alias("next-page"), - "w": cc.Alias("put"), - "q": cc.Alias("quit"), - }, - }) + */ root := layout.FullScreen(tableSelect) From cb908ec4eb8ee77fde6720d07ab839c851a395c2 Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Fri, 16 May 2025 18:01:28 +1000 Subject: [PATCH 05/13] Added back the core interactive commands --- cmd/dynamo-browse/main.go | 5 +- .../common/ui/commandctrl/cmdpacks/stdcmds.go | 324 +++++++++++++++++- internal/common/ui/commandctrl/commandctrl.go | 5 + internal/common/ui/commandctrl/ctx.go | 9 + internal/common/ui/commandctrl/iface.go | 4 + internal/dynamo-browse/ui/model.go | 4 + 6 files changed, 347 insertions(+), 4 deletions(-) diff --git a/cmd/dynamo-browse/main.go b/cmd/dynamo-browse/main.go index 7b11624..820abc0 100644 --- a/cmd/dynamo-browse/main.go +++ b/cmd/dynamo-browse/main.go @@ -162,7 +162,9 @@ func main() { commandController := commandctrl.NewCommandController(inputHistoryService, cmdpacks.StandardCommands{ - ReadController: tableReadController, + ReadController: tableReadController, + WriteController: tableWriteController, + ExportController: exportController, }, ) commandController.AddCommandLookupExtension(scriptController) @@ -183,6 +185,7 @@ func main() { pasteboardProvider, keyBindings, ) + commandController.SetUIStateProvider(&model) // Pre-determine if layout has dark background. This prevents calls for creating a list to hang. osstyle.DetectCurrentScheme() diff --git a/internal/common/ui/commandctrl/cmdpacks/stdcmds.go b/internal/common/ui/commandctrl/cmdpacks/stdcmds.go index 610d750..63cbcfa 100644 --- a/internal/common/ui/commandctrl/cmdpacks/stdcmds.go +++ b/internal/common/ui/commandctrl/cmdpacks/stdcmds.go @@ -5,17 +5,21 @@ import ( tea "github.com/charmbracelet/bubbletea" "github.com/lmika/dynamo-browse/internal/common/ui/commandctrl" "github.com/lmika/dynamo-browse/internal/dynamo-browse/controllers" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" + "github.com/pkg/errors" "ucl.lmika.dev/repl" "ucl.lmika.dev/ucl" ) type StandardCommands struct { - ReadController *controllers.TableReadController + ReadController *controllers.TableReadController + WriteController *controllers.TableWriteController + ExportController *controllers.ExportController + KeyBindingController *controllers.KeyBindingController } var cmdQuitDoc = repl.Doc{ Brief: "Quits dynamo-browse", - Usage: "quit", Detailed: ` This will quit dynamo-browse immediately, without prompting to apply any changes. @@ -29,7 +33,7 @@ func (sc StandardCommands) cmdQuit(ctx context.Context, args ucl.CallArgs) (any, var cmdTableDoc = repl.Doc{ Brief: "Prompt for table to scan", - Usage: "table [NAME]", + Usage: "[NAME]", Args: []repl.ArgDoc{ {Name: "name", Brief: "Name of the table to scan"}, }, @@ -54,7 +58,321 @@ func (sc StandardCommands) cmdTable(ctx context.Context, args ucl.CallArgs) (any return nil, nil } +var cmdExportDoc = repl.Doc{ + Brief: "Exports a result-set as CSV", + Usage: "FILENAME [-all]", + Args: []repl.ArgDoc{ + {Name: "filename", Brief: "Filename to export to"}, + {Name: "-all", Brief: "Export all results from the table"}, + }, + Detailed: ` + The fields of the current table view will be treated as the header of the + exported table. + + This command is intended only for interactive sessions and is not suitable + for scripting. The export will run asynchronously. + `, +} + +func (sc StandardCommands) cmdExport(ctx context.Context, args ucl.CallArgs) (any, error) { + var filename string + if err := args.Bind(&filename); err != nil { + return nil, errors.New("expected filename") + } + + opts := controllers.ExportOptions{ + AllResults: args.HasSwitch("all"), + } + + commandctrl.PostMsg(ctx, sc.ExportController.ExportCSV(filename, opts)) + return nil, nil +} + +var cmdMarkDoc = repl.Doc{ + Brief: "Set the marked items of the current result-set", + Usage: "[WHAT] [-where EXPR]", + Args: []repl.ArgDoc{ + {Name: "what", Brief: "Items to mark. Defaults to 'all'"}, + {Name: "-where", Brief: "Filter expression select items to mark"}, + }, + Detailed: ` + WHAT can be one of: + + - all: Mark all items in the current result-set + - none: Unmark all items in the current result-set + - toggle: Toggle the marked state of all items in the current result-set + `, +} + +func (sc StandardCommands) cmdMark(ctx context.Context, args ucl.CallArgs) (any, error) { + var markOp = controllers.MarkOpMark + + if args.NArgs() > 0 { + var markOpStr string + if err := args.Bind(&markOpStr); err == nil { + switch markOpStr { + case "all": + markOp = controllers.MarkOpMark + case "none": + markOp = controllers.MarkOpUnmark + case "toggle": + markOp = controllers.MarkOpToggle + default: + return nil, errors.New("unrecognised mark operation") + } + } + } + + var whereExpr = "" + if args.HasSwitch("where") { + if err := args.BindSwitch("where", &whereExpr); err != nil { + return nil, err + } + } + + commandctrl.PostMsg(ctx, sc.ReadController.Mark(markOp, whereExpr)) + return nil, nil +} + +var cmdNextPageDoc = repl.Doc{ + Brief: "Retrieve and display the next page of the current result-set", + Detailed: ` + This command is intended only for interactive sessions and is not suitable + for scripting. Fetching the next page will run asynchronously. + `, +} + +func (sc StandardCommands) cmdNextPage(ctx context.Context, args ucl.CallArgs) (any, error) { + commandctrl.PostMsg(ctx, sc.ReadController.NextPage()) + return nil, nil +} + +var cmdDeleteDoc = repl.Doc{ + Brief: "Delete the marked items of the current result-set", + Detailed: ` + The user will be prompted to confirm the deletion. If approved, the + items will be deleted immediately. + + This command is intended only for interactive sessions and is not suitable + for scripting. + `, +} + +func (sc StandardCommands) cmdDelete(ctx context.Context, args ucl.CallArgs) (any, error) { + commandctrl.PostMsg(ctx, sc.WriteController.DeleteMarked()) + return nil, nil +} + +var cmdNewItemDoc = repl.Doc{ + Brief: "Adds a new item to the current result-set", + Detailed: ` + The user will be prompted to enter the values for each required attribute, + such as the partition and sort key. The new item will be commited to the database + upon the next write. + + This command is intended only for interactive sessions and is not suitable + for scripting. + `, +} + +func (sc StandardCommands) cmdNewItem(ctx context.Context, args ucl.CallArgs) (any, error) { + commandctrl.PostMsg(ctx, sc.WriteController.NewItem()) + return nil, nil +} + +var cmdCloneDoc = repl.Doc{ + Brief: "Adds a copy of the selected item as a new item to the current result-set", + Detailed: ` + The user will be prompted to enter the partition and sort key. All other + attributes will be cloned from the selected item. The new item will be + commited to the database upon the next write. + + This command is intended only for interactive sessions and is not suitable + for scripting. + `, +} + +func (sc StandardCommands) cmdClone(ctx context.Context, args ucl.CallArgs) (any, error) { + selectedItemIndex, ok := commandctrl.SelectedItemIndex(ctx) + if !ok { + return nil, errors.New("no item selected") + } + + commandctrl.PostMsg(ctx, sc.WriteController.CloneItem(selectedItemIndex)) + return nil, nil +} + +var cmdSetAttrDoc = repl.Doc{ + Brief: "Modify a field value of the selected or marked items", + Usage: "ATTR [TYPE]", + Args: []repl.ArgDoc{ + {Name: "attr", Brief: "Attribute to modify"}, + {Name: "-S", Brief: "Set attribute to a string"}, + {Name: "-N", Brief: "Set attribute to a number"}, + {Name: "-BOOL", Brief: "Set attribute to a boolean"}, + {Name: "-NULL", Brief: "Set attribute to a null"}, + {Name: "-TO", Brief: "Set attribute to the result of an expression"}, + }, + Detailed: ` + The user will be prompted to enter the new value for the attribute. + If the attribute type is not known, then a type will need to be specified. + Otherwise, the type will be unchanged. The modified item will be + commited to the database upon the next write. + `, +} + +func (sc StandardCommands) cmdSetAttr(ctx context.Context, args ucl.CallArgs) (any, error) { + var fieldName string + if err := args.Bind(&fieldName); err != nil { + return nil, errors.New("expected field name") + } + + selectedItemIndex, ok := commandctrl.SelectedItemIndex(ctx) + if !ok { + return nil, errors.New("no item selected") + } + + var itemType = models.UnsetItemType + switch { + case args.HasSwitch("S"): + itemType = models.StringItemType + case args.HasSwitch("N"): + itemType = models.NumberItemType + case args.HasSwitch("BOOL"): + itemType = models.BoolItemType + case args.HasSwitch("NULL"): + itemType = models.NullItemType + case args.HasSwitch("TO"): + itemType = models.ExprValueItemType + } + + commandctrl.PostMsg(ctx, sc.WriteController.SetAttributeValue(selectedItemIndex, itemType, fieldName)) + return nil, nil +} + +var cmdDelAttrDoc = repl.Doc{ + Brief: "Remove the field of the selected or marked items", + Usage: "ATTR", + Args: []repl.ArgDoc{ + {Name: "attr", Brief: "Attribute to remove"}, + }, + Detailed: ` + The modified item will be commited to the database upon the next write. + `, +} + +func (sc StandardCommands) cmdDelAttr(ctx context.Context, args ucl.CallArgs) (any, error) { + var fieldName string + if err := args.Bind(&fieldName); err != nil { + return nil, errors.New("expected field name") + } + + selectedItemIndex, ok := commandctrl.SelectedItemIndex(ctx) + if !ok { + return nil, errors.New("no item selected") + } + + commandctrl.PostMsg(ctx, sc.WriteController.DeleteAttribute(selectedItemIndex, fieldName)) + return nil, nil +} + +var cmdPutDoc = repl.Doc{ + Brief: "Commit changes to the table", + Detailed: ` + This will put all new and modified items. + + This command is intended only for interactive sessions and is not suitable + for scripting. The user will be prompted to confirm the changes. + `, +} + +func (sc StandardCommands) cmdPut(ctx context.Context, args ucl.CallArgs) (any, error) { + commandctrl.PostMsg(ctx, sc.WriteController.PutItems()) + return nil, nil +} + +var cmdTouchDoc = repl.Doc{ + Brief: "Put the currently selected item", + Detailed: ` + This will put the currently selected item, regardless of whether it has been + modified. + + This command is intended only for interactive sessions and is not suitable + for scripting. The user will be prompted to confirm the touch. + `, +} + +func (sc StandardCommands) cmdTouch(ctx context.Context, args ucl.CallArgs) (any, error) { + selectedItemIndex, ok := commandctrl.SelectedItemIndex(ctx) + if !ok { + return nil, errors.New("no item selected") + } + + commandctrl.PostMsg(ctx, sc.WriteController.TouchItem(selectedItemIndex)) + return nil, nil +} + +var cmdNoisyTouchDoc = repl.Doc{ + Brief: "Put the currently selected item by deleting it first", + Detailed: ` + This will put the currently selected item, regardless of whether it has been + modified. It does so by removing the item from the table, then adding it back again. + + This command is intended only for interactive sessions and is not suitable + for scripting. The user will be prompted to confirm the touch. + `, +} + +func (sc StandardCommands) cmdNoisyTouch(ctx context.Context, args ucl.CallArgs) (any, error) { + selectedItemIndex, ok := commandctrl.SelectedItemIndex(ctx) + if !ok { + return nil, errors.New("no item selected") + } + + commandctrl.PostMsg(ctx, sc.WriteController.NoisyTouchItem(selectedItemIndex)) + return nil, nil +} + +var cmdRebindDoc = repl.Doc{ + Brief: "Binds a new key to an action", + Usage: "ACTION KEY", + Args: []repl.ArgDoc{ + {Name: "action", Brief: "Action to bind"}, + {Name: "key", Brief: "Key to bind"}, + }, + Detailed: ` + If the key is already bound to an action, it will be replaced. + The set of actions this command accepts is well-defined. For binding + to arbitrary actions, use the ui:bind command. + `, +} + +func (sc StandardCommands) cmdRebind(ctx context.Context, args ucl.CallArgs) (any, error) { + var bindingName, newKey string + if err := args.Bind(&bindingName, &newKey); err != nil { + return nil, errors.New("expected: bindingName newKey") + } + + // TODO: should only force if not interactive + commandctrl.PostMsg(ctx, sc.KeyBindingController.Rebind(bindingName, newKey, false)) + return nil, nil +} + func (sc StandardCommands) ConfigureUCL(ucl *ucl.Inst) { ucl.SetBuiltin("quit", sc.cmdQuit) ucl.SetBuiltin("table", sc.cmdTable) + ucl.SetBuiltin("export", sc.cmdExport) + ucl.SetBuiltin("mark", sc.cmdMark) + // unmark --> alias for { mark none } + ucl.SetBuiltin("next-page", sc.cmdNextPage) + ucl.SetBuiltin("delete", sc.cmdDelete) + ucl.SetBuiltin("new-item", sc.cmdNewItem) + ucl.SetBuiltin("clone", sc.cmdClone) + ucl.SetBuiltin("set-attr", sc.cmdSetAttr) + ucl.SetBuiltin("del-attr", sc.cmdDelAttr) + ucl.SetBuiltin("put", sc.cmdPut) + ucl.SetBuiltin("touch", sc.cmdTouch) + ucl.SetBuiltin("noisy-touch", sc.cmdNoisyTouch) + ucl.SetBuiltin("rebind", sc.cmdRebind) + // set-opt --> alias to opts:set } diff --git a/internal/common/ui/commandctrl/commandctrl.go b/internal/common/ui/commandctrl/commandctrl.go index 8c110a4..99bec81 100644 --- a/internal/common/ui/commandctrl/commandctrl.go +++ b/internal/common/ui/commandctrl/commandctrl.go @@ -28,6 +28,7 @@ type CommandController struct { commandList *CommandList lookupExtensions []CommandLookupExtension completionProvider CommandCompletionProvider + uiStateProvider UIStateProvider cmdChan chan cmdMessage msgChan chan tea.Msg interactive bool @@ -68,6 +69,10 @@ func (c *CommandController) StartMessageSender(msgSender func(tea.Msg)) { } } +func (c *CommandController) SetUIStateProvider(provider UIStateProvider) { + c.uiStateProvider = provider +} + func (c *CommandController) AddCommandLookupExtension(ext CommandLookupExtension) { c.lookupExtensions = append(c.lookupExtensions, ext) } diff --git a/internal/common/ui/commandctrl/ctx.go b/internal/common/ui/commandctrl/ctx.go index a041e33..7c6f193 100644 --- a/internal/common/ui/commandctrl/ctx.go +++ b/internal/common/ui/commandctrl/ctx.go @@ -15,3 +15,12 @@ func PostMsg(ctx context.Context, msg tea.Msg) { cmdCtl.postMessage(msg) } } + +func SelectedItemIndex(ctx context.Context) (int, bool) { + cmdCtl, ok := ctx.Value(commandCtlKey).(*CommandController) + if !ok { + return 0, false + } + + return cmdCtl.uiStateProvider.SelectedItemIndex(), true +} diff --git a/internal/common/ui/commandctrl/iface.go b/internal/common/ui/commandctrl/iface.go index 1cb834a..41d3a7f 100644 --- a/internal/common/ui/commandctrl/iface.go +++ b/internal/common/ui/commandctrl/iface.go @@ -8,3 +8,7 @@ import ( type IterProvider interface { Iter(ctx context.Context, category string) services.HistoryProvider } + +type UIStateProvider interface { + SelectedItemIndex() int +} diff --git a/internal/dynamo-browse/ui/model.go b/internal/dynamo-browse/ui/model.go index d6c0a8e..d57c816 100644 --- a/internal/dynamo-browse/ui/model.go +++ b/internal/dynamo-browse/ui/model.go @@ -393,3 +393,7 @@ func (m *Model) promptToQuit() tea.Msg { return nil }) } + +func (m *Model) SelectedItemIndex() int { + return m.tableView.SelectedItemIndex() +} From 6bf721873b6bfe78ddaf41556ab83cc5345bfb8d Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Sat, 17 May 2025 11:11:04 +1000 Subject: [PATCH 06/13] Added rs:new and rs:query --- cmd/dynamo-browse/main.go | 9 +- go.mod | 6 +- go.sum | 4 + .../common/ui/commandctrl/cmdpacks/modrs.go | 123 +++++++++++++++ .../ui/commandctrl/cmdpacks/modrs_test.go | 80 ++++++++++ .../common/ui/commandctrl/cmdpacks/proxy.go | 7 + .../common/ui/commandctrl/cmdpacks/stdcmds.go | 9 ++ .../ui/commandctrl/cmdpacks/stdcmds_test.go | 143 ++++++++++++++++++ internal/common/ui/commandctrl/commandctrl.go | 26 ++-- internal/common/ui/commandctrl/packs.go | 1 + .../dynamo-browse/models/queryexpr/types.go | 35 +++++ 11 files changed, 424 insertions(+), 19 deletions(-) create mode 100644 internal/common/ui/commandctrl/cmdpacks/modrs.go create mode 100644 internal/common/ui/commandctrl/cmdpacks/modrs_test.go create mode 100644 internal/common/ui/commandctrl/cmdpacks/proxy.go create mode 100644 internal/common/ui/commandctrl/cmdpacks/stdcmds_test.go diff --git a/cmd/dynamo-browse/main.go b/cmd/dynamo-browse/main.go index 820abc0..aeaf4eb 100644 --- a/cmd/dynamo-browse/main.go +++ b/cmd/dynamo-browse/main.go @@ -162,9 +162,12 @@ func main() { commandController := commandctrl.NewCommandController(inputHistoryService, cmdpacks.StandardCommands{ - ReadController: tableReadController, - WriteController: tableWriteController, - ExportController: exportController, + TableService: tableService, + State: state, + ReadController: tableReadController, + WriteController: tableWriteController, + ExportController: exportController, + KeyBindingController: keyBindingController, }, ) commandController.AddCommandLookupExtension(scriptController) diff --git a/go.mod b/go.mod index 4f87cec..3678e7c 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,8 @@ module github.com/lmika/dynamo-browse -go 1.22 +go 1.24 -toolchain go1.22.0 +toolchain go1.24.0 require ( github.com/alecthomas/participle/v2 v2.1.1 @@ -117,5 +117,5 @@ require ( golang.org/x/text v0.9.0 // indirect google.golang.org/appengine v1.6.7 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - ucl.lmika.dev v0.0.0-20250306030053-ad6d002a22e8 // indirect + ucl.lmika.dev v0.0.0-20250517003439-109be33d1495 // indirect ) diff --git a/go.sum b/go.sum index f26fcfa..fdaef5d 100644 --- a/go.sum +++ b/go.sum @@ -438,3 +438,7 @@ ucl.lmika.dev v0.0.0-20240504013531-0dc9fd3c3281 h1:/M7phiv/0XVp3wKkOxEnGQysf8+R ucl.lmika.dev v0.0.0-20240504013531-0dc9fd3c3281/go.mod h1:T6V4jIUxlWvMTgn4J752VDHNA8iyVrEX6v98EvDj8G4= ucl.lmika.dev v0.0.0-20250306030053-ad6d002a22e8 h1:vWttdW8GJWcTUQeJFbQHqCHJDLFWQ9nccUTx/lW2v8s= ucl.lmika.dev v0.0.0-20250306030053-ad6d002a22e8/go.mod h1:FMP2ncSu4UxfvB0iA2zlebwL+1UPCARdyYNOrmi86A4= +ucl.lmika.dev v0.0.0-20250515115457-27b6cc0b92e2 h1:cvguOoQ0HVgLKbHH17ZHvAUFht6HXApLi0o8JOdaaNU= +ucl.lmika.dev v0.0.0-20250515115457-27b6cc0b92e2/go.mod h1:/MMZKm6mOMtnY4I8TYEot4Pc8dKEy+/IAQo1VdpA5EY= +ucl.lmika.dev v0.0.0-20250517003439-109be33d1495 h1:r46r+7T59Drm+in7TEWKCZfFYIM0ZyZ26QjHAbj8Lto= +ucl.lmika.dev v0.0.0-20250517003439-109be33d1495/go.mod h1:/MMZKm6mOMtnY4I8TYEot4Pc8dKEy+/IAQo1VdpA5EY= diff --git a/internal/common/ui/commandctrl/cmdpacks/modrs.go b/internal/common/ui/commandctrl/cmdpacks/modrs.go new file mode 100644 index 0000000..a748899 --- /dev/null +++ b/internal/common/ui/commandctrl/cmdpacks/modrs.go @@ -0,0 +1,123 @@ +package cmdpacks + +import ( + "context" + "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/controllers" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models/queryexpr" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/tables" + "github.com/pkg/errors" + "ucl.lmika.dev/repl" + "ucl.lmika.dev/ucl" +) + +type rsModule struct { + tableService *tables.Service + state *controllers.State +} + +var rsNewDoc = repl.Doc{ + Brief: "Creates a new, empty result set", +} + +func (rs *rsModule) rsNew(ctx context.Context, args ucl.CallArgs) (any, error) { + return &ResultSetProxy{ + RS: &models.ResultSet{}, + }, nil +} + +var rsQueryDoc = repl.Doc{ + Brief: "Runs a query and returns the results as a result-set", + Usage: "QUERY [ARGS] [-table NAME]", + Args: []repl.ArgDoc{ + {Name: "query", Brief: "Query expression to run"}, + {Name: "args", Brief: "Hash of argument values to substitute into the query"}, + {Name: "-table", Brief: "Optional table name to use for the query"}, + }, + Detailed: ` + If no table is specified, then the value of @table will be used. If this is unavailable, + the command will return an error. + `, +} + +func (rs *rsModule) rsQuery(ctx context.Context, args ucl.CallArgs) (any, error) { + var expr string + if err := args.Bind(&expr); err != nil { + return nil, err + } + + q, err := queryexpr.Parse(expr) + if err != nil { + return nil, err + } + + if args.NArgs() > 0 { + var queryArgs ucl.Hashable + if err := args.Bind(&queryArgs); err != nil { + return nil, err + } + + queryNames := map[string]string{} + queryValues := map[string]types.AttributeValue{} + queryArgs.Each(func(k string, v ucl.Object) error { + if v == nil { + return nil + } + + queryNames[k] = v.String() + + switch v.(type) { + case ucl.StringObject: + queryValues[k] = &types.AttributeValueMemberS{Value: v.String()} + case ucl.IntObject: + queryValues[k] = &types.AttributeValueMemberN{Value: v.String()} + // TODO: other types + } + return nil + }) + + q = q.WithNameParams(queryNames).WithValueParams(queryValues) + } + + var tableInfo *models.TableInfo + if args.HasSwitch("table") { + var tblName string + if err := args.BindSwitch("table", &tblName); err != nil { + return nil, err + } + + tableInfo, err = rs.tableService.Describe(ctx, tblName) + if err != nil { + return nil, err + } + } else if currRs := rs.state.ResultSet(); currRs != nil && currRs.TableInfo != nil { + tableInfo = currRs.TableInfo + } else { + return nil, errors.New("no table specified") + } + + newResultSet, err := rs.tableService.ScanOrQuery(context.Background(), tableInfo, q, nil) + if err != nil { + return nil, err + } + + return &ResultSetProxy{ + RS: newResultSet, + }, nil +} + +func moduleRS(tableService *tables.Service, state *controllers.State) ucl.Module { + m := &rsModule{ + tableService: tableService, + state: state, + } + + return ucl.Module{ + Name: "rs", + Builtins: map[string]ucl.BuiltinHandler{ + "new": m.rsNew, + "query": m.rsQuery, + }, + } +} diff --git a/internal/common/ui/commandctrl/cmdpacks/modrs_test.go b/internal/common/ui/commandctrl/cmdpacks/modrs_test.go new file mode 100644 index 0000000..113bd46 --- /dev/null +++ b/internal/common/ui/commandctrl/cmdpacks/modrs_test.go @@ -0,0 +1,80 @@ +package cmdpacks_test + +import ( + "fmt" + "github.com/lmika/dynamo-browse/internal/common/ui/commandctrl/cmdpacks" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestModRS_New(t *testing.T) { + svc := newService(t) + + rsProxy, err := svc.CommandController.ExecuteAndWait(t.Context(), `rs:new`) + + assert.NoError(t, err) + assert.IsType(t, rsProxy, &cmdpacks.ResultSetProxy{}) +} + +func TestModRS_Query(t *testing.T) { + tests := []struct { + descr string + cmd string + wantRows []int + }{ + { + descr: "query with pk 1", + cmd: `rs:query 'pk="abc"' -table service-test-data`, + wantRows: []int{0, 1}, + }, + { + descr: "query with pk 2", + cmd: `rs:query 'pk="bbb"' -table service-test-data`, + wantRows: []int{2}, + }, + { + descr: "query with sk 1", + cmd: `rs:query 'sk="222"' -table service-test-data`, + wantRows: []int{1}, + }, + { + descr: "query with args 1", + cmd: `rs:query 'pk=$v' [v:'abc'] -table service-test-data`, + wantRows: []int{0, 1}, + }, + { + descr: "query with args 2", + cmd: `rs:query ':k=$v' [k:'pk' v:'abc'] -table service-test-data`, + wantRows: []int{0, 1}, + }, + { + descr: "query with args 3", + cmd: `rs:query ':k=$v' [k:'beta' v:1231] -table service-test-data`, + wantRows: []int{1}, + }, + { + descr: "query with args with no table set", + cmd: `rs:query ':k=$v' [k:'beta' v:1231]`, + wantRows: []int{1}, + }, + } + for _, tt := range tests { + t.Run(tt.descr, func(t *testing.T) { + svc := newService(t) + + res, err := svc.CommandController.ExecuteAndWait(t.Context(), tt.cmd) + assert.NoError(t, err) + + rs := res.(*cmdpacks.ResultSetProxy).RS + assert.Len(t, rs.Items(), len(tt.wantRows)) + + for i, rowIndex := range tt.wantRows { + for key, want := range testData[0].Data[rowIndex] { + have, ok := rs.Items()[i].AttributeValueAsString(key) + assert.True(t, ok) + assert.Equal(t, fmt.Sprint(want), have) + } + } + }) + } +} diff --git a/internal/common/ui/commandctrl/cmdpacks/proxy.go b/internal/common/ui/commandctrl/cmdpacks/proxy.go new file mode 100644 index 0000000..b80aab2 --- /dev/null +++ b/internal/common/ui/commandctrl/cmdpacks/proxy.go @@ -0,0 +1,7 @@ +package cmdpacks + +import "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" + +type ResultSetProxy struct { + RS *models.ResultSet +} diff --git a/internal/common/ui/commandctrl/cmdpacks/stdcmds.go b/internal/common/ui/commandctrl/cmdpacks/stdcmds.go index 63cbcfa..21756a3 100644 --- a/internal/common/ui/commandctrl/cmdpacks/stdcmds.go +++ b/internal/common/ui/commandctrl/cmdpacks/stdcmds.go @@ -6,12 +6,15 @@ import ( "github.com/lmika/dynamo-browse/internal/common/ui/commandctrl" "github.com/lmika/dynamo-browse/internal/dynamo-browse/controllers" "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/tables" "github.com/pkg/errors" "ucl.lmika.dev/repl" "ucl.lmika.dev/ucl" ) type StandardCommands struct { + TableService *tables.Service + State *controllers.State ReadController *controllers.TableReadController WriteController *controllers.TableWriteController ExportController *controllers.ExportController @@ -358,6 +361,12 @@ func (sc StandardCommands) cmdRebind(ctx context.Context, args ucl.CallArgs) (an return nil, nil } +func (sc StandardCommands) InstOptions() []ucl.InstOption { + return []ucl.InstOption{ + ucl.WithModule(moduleRS(sc.TableService, sc.State)), + } +} + func (sc StandardCommands) ConfigureUCL(ucl *ucl.Inst) { ucl.SetBuiltin("quit", sc.cmdQuit) ucl.SetBuiltin("table", sc.cmdTable) diff --git a/internal/common/ui/commandctrl/cmdpacks/stdcmds_test.go b/internal/common/ui/commandctrl/cmdpacks/stdcmds_test.go new file mode 100644 index 0000000..48dd158 --- /dev/null +++ b/internal/common/ui/commandctrl/cmdpacks/stdcmds_test.go @@ -0,0 +1,143 @@ +package cmdpacks_test + +import ( + "github.com/lmika/dynamo-browse/internal/common/ui/commandctrl" + "github.com/lmika/dynamo-browse/internal/common/ui/commandctrl/cmdpacks" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/controllers" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/providers/dynamo" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/providers/inputhistorystore" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/providers/pasteboardprovider" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/providers/settingstore" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/providers/workspacestore" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/inputhistory" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/itemrenderer" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/jobs" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/tables" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/viewsnapshot" + "github.com/lmika/dynamo-browse/test/testdynamo" + "github.com/lmika/dynamo-browse/test/testworkspace" + bus "github.com/lmika/events" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestStdCmds_Mark(t *testing.T) { + tests := []struct { + descr string + cmd string + wantMarks []bool + }{ + {descr: "mark default", cmd: "mark", wantMarks: []bool{true, true, true}}, + {descr: "mark all", cmd: "mark all", wantMarks: []bool{true, true, true}}, + {descr: "mark none", cmd: "mark none", wantMarks: []bool{false, false, false}}, + {descr: "mark where", cmd: `mark -where 'sk="222"'`, wantMarks: []bool{false, true, false}}, + {descr: "mark toggle", cmd: "mark ; mark toggle", wantMarks: []bool{false, false, false}}, + } + + for _, tt := range tests { + t.Run(tt.descr, func(t *testing.T) { + svc := newService(t) + + _, err := svc.CommandController.ExecuteAndWait(t.Context(), tt.cmd) + assert.NoError(t, err) + + for i, want := range tt.wantMarks { + assert.Equal(t, want, svc.State.ResultSet().Marked(i)) + } + }) + } +} + +type services struct { + CommandController *commandctrl.CommandController + SelItemIndex int + + State *controllers.State +} + +func newService(t *testing.T) *services { + ws := testworkspace.New(t) + + resultSetSnapshotStore := workspacestore.NewResultSetSnapshotStore(ws) + settingStore := settingstore.New(ws) + inputHistoryStore := inputhistorystore.NewInputHistoryStore(ws) + + workspaceService := viewsnapshot.NewService(resultSetSnapshotStore) + itemRendererService := itemrenderer.NewService(itemrenderer.PlainTextRenderer(), itemrenderer.PlainTextRenderer()) + inputHistoryService := inputhistory.New(inputHistoryStore) + + client := testdynamo.SetupTestTable(t, testData) + + provider := dynamo.NewProvider(client) + service := tables.NewService(provider, settingStore) + eventBus := bus.New() + + state := controllers.NewState() + jobsController := controllers.NewJobsController(jobs.NewService(eventBus), eventBus, true) + readController := controllers.NewTableReadController( + state, + service, + workspaceService, + itemRendererService, + jobsController, + inputHistoryService, + eventBus, + pasteboardprovider.NilProvider{}, + nil, + "service-test-data", + ) + writeController := controllers.NewTableWriteController(state, service, jobsController, readController, settingStore) + settingsController := controllers.NewSettingsController(settingStore, eventBus) + columnsController := controllers.NewColumnsController(readController, eventBus) + exportController := controllers.NewExportController(state, service, jobsController, columnsController, pasteboardprovider.NilProvider{}) + + _ = settingsController + commandController := commandctrl.NewCommandController(inputHistoryService, + cmdpacks.StandardCommands{ + State: state, + TableService: service, + ReadController: readController, + WriteController: writeController, + ExportController: exportController, + }, + ) + + s := &services{ + State: state, + CommandController: commandController, + } + + commandController.SetUIStateProvider(s) + readController.Init() + + return s +} + +func (s *services) SelectedItemIndex() int { + return s.SelItemIndex +} + +var testData = []testdynamo.TestData{ + { + TableName: "service-test-data", + Data: []map[string]interface{}{ + { + "pk": "abc", + "sk": "111", + "alpha": "This is some value", + }, + { + "pk": "abc", + "sk": "222", + "alpha": "This is another some value", + "beta": 1231, + }, + { + "pk": "bbb", + "sk": "131", + "beta": 2468, + "gamma": "foobar", + }, + }, + }, +} diff --git a/internal/common/ui/commandctrl/commandctrl.go b/internal/common/ui/commandctrl/commandctrl.go index 99bec81..79847f0 100644 --- a/internal/common/ui/commandctrl/commandctrl.go +++ b/internal/common/ui/commandctrl/commandctrl.go @@ -43,11 +43,17 @@ func NewCommandController(historyProvider IterProvider, pkgs ...CommandPack) *Co msgChan: make(chan tea.Msg), interactive: true, } - cc.uclInst = ucl.New( + + options := []ucl.InstOption{ ucl.WithOut(ucl.LineHandler(cc.printLine)), ucl.WithModule(builtins.OS()), ucl.WithModule(builtins.FS(nil)), - ) + } + for _, pkg := range pkgs { + options = append(options, pkg.InstOptions()...) + } + + cc.uclInst = ucl.New(options...) for _, pkg := range pkgs { pkg.ConfigureUCL(cc.uclInst) @@ -126,26 +132,20 @@ func (c *CommandController) execute(ctx ExecContext, commandInput string) tea.Ms return events.Error(errors.New("command currently running")) } - /* - res, err := c.uclInst.Eval(context.Background(), commandInput) - if err != nil { - return events.Error(err) - } - - if teaMsg, ok := res.(teaMsgWrapper); ok { - return teaMsg.msg - } - */ return nil } +func (c *CommandController) ExecuteAndWait(ctx context.Context, commandInput string) (any, error) { + return c.uclInst.Eval(ctx, commandInput) +} + func (c *CommandController) cmdLooper() { ctx := context.WithValue(context.Background(), commandCtlKey, c) for { select { case cmdChan := <-c.cmdChan: - res, err := c.uclInst.Eval(ctx, cmdChan.cmd) + res, err := c.ExecuteAndWait(ctx, cmdChan.cmd) if err != nil { c.postMessage(events.Error(err)) } else if res != nil { diff --git a/internal/common/ui/commandctrl/packs.go b/internal/common/ui/commandctrl/packs.go index 76d0bd7..6f5dbc6 100644 --- a/internal/common/ui/commandctrl/packs.go +++ b/internal/common/ui/commandctrl/packs.go @@ -3,5 +3,6 @@ package commandctrl import "ucl.lmika.dev/ucl" type CommandPack interface { + InstOptions() []ucl.InstOption ConfigureUCL(ucl *ucl.Inst) } diff --git a/internal/dynamo-browse/models/queryexpr/types.go b/internal/dynamo-browse/models/queryexpr/types.go index 2e55c7a..011931e 100644 --- a/internal/dynamo-browse/models/queryexpr/types.go +++ b/internal/dynamo-browse/models/queryexpr/types.go @@ -8,6 +8,7 @@ import ( "github.com/pkg/errors" "math/big" "strconv" + "strings" ) type exprValue interface { @@ -62,6 +63,14 @@ func newExprValueFromAttributeValue(ev types.AttributeValue) (exprValue, error) case *types.AttributeValueMemberS: return stringExprValue(xVal.Value), nil case *types.AttributeValueMemberN: + if !strings.Contains(xVal.Value, ".") { + iVal, err := strconv.ParseInt(xVal.Value, 10, 64) + if err != nil { + return nil, err + } + return int64ExprValue(iVal), nil + } + xNumVal, _, err := big.ParseFloat(xVal.Value, 10, 63, big.ToNearestEven) if err != nil { return nil, err @@ -139,6 +148,32 @@ func (s int64ExprValue) typeName() string { return "N" } +type bigIntExprValue struct { + num *big.Int +} + +func (i bigIntExprValue) asGoValue() any { + return i.num +} + +func (i bigIntExprValue) asAttributeValue() types.AttributeValue { + return &types.AttributeValueMemberN{Value: i.num.String()} +} + +func (i bigIntExprValue) asInt() int64 { + return i.num.Int64() +} + +func (i bigIntExprValue) asBigFloat() *big.Float { + var f big.Float + f.SetInt64(i.num.Int64()) + return &f +} + +func (s bigIntExprValue) typeName() string { + return "N" +} + type bigNumExprValue struct { num *big.Float } From 18ffe85a562bc77e4f29bdc75812f316d1c68d01 Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Sat, 17 May 2025 22:16:49 +1000 Subject: [PATCH 07/13] First attempt at a resultset pseudovar The resultset needs a table set, so rs:new will also assume the current table. --- go.mod | 2 +- go.sum | 2 ++ .../common/ui/commandctrl/cmdpacks/modrs.go | 34 ++++++++++++++++--- .../common/ui/commandctrl/cmdpacks/pvars.go | 31 +++++++++++++++++ .../common/ui/commandctrl/cmdpacks/stdcmds.go | 2 ++ internal/dynamo-browse/controllers/state.go | 8 +++++ .../dynamo-browse/controllers/tableread.go | 6 ++++ test/testdynamo/client.go | 8 ++++- 8 files changed, 87 insertions(+), 6 deletions(-) create mode 100644 internal/common/ui/commandctrl/cmdpacks/pvars.go diff --git a/go.mod b/go.mod index 3678e7c..51c142d 100644 --- a/go.mod +++ b/go.mod @@ -117,5 +117,5 @@ require ( golang.org/x/text v0.9.0 // indirect google.golang.org/appengine v1.6.7 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - ucl.lmika.dev v0.0.0-20250517003439-109be33d1495 // indirect + ucl.lmika.dev v0.0.0-20250517115116-0f1ceba0902e // indirect ) diff --git a/go.sum b/go.sum index fdaef5d..1e50447 100644 --- a/go.sum +++ b/go.sum @@ -442,3 +442,5 @@ ucl.lmika.dev v0.0.0-20250515115457-27b6cc0b92e2 h1:cvguOoQ0HVgLKbHH17ZHvAUFht6H ucl.lmika.dev v0.0.0-20250515115457-27b6cc0b92e2/go.mod h1:/MMZKm6mOMtnY4I8TYEot4Pc8dKEy+/IAQo1VdpA5EY= ucl.lmika.dev v0.0.0-20250517003439-109be33d1495 h1:r46r+7T59Drm+in7TEWKCZfFYIM0ZyZ26QjHAbj8Lto= ucl.lmika.dev v0.0.0-20250517003439-109be33d1495/go.mod h1:/MMZKm6mOMtnY4I8TYEot4Pc8dKEy+/IAQo1VdpA5EY= +ucl.lmika.dev v0.0.0-20250517115116-0f1ceba0902e h1:CQ+qPqI5lYiiEM0tNAr4jS0iMz16bFqOui5mU3AHsCU= +ucl.lmika.dev v0.0.0-20250517115116-0f1ceba0902e/go.mod h1:/MMZKm6mOMtnY4I8TYEot4Pc8dKEy+/IAQo1VdpA5EY= diff --git a/internal/common/ui/commandctrl/cmdpacks/modrs.go b/internal/common/ui/commandctrl/cmdpacks/modrs.go index a748899..4af1513 100644 --- a/internal/common/ui/commandctrl/cmdpacks/modrs.go +++ b/internal/common/ui/commandctrl/cmdpacks/modrs.go @@ -8,6 +8,7 @@ import ( "github.com/lmika/dynamo-browse/internal/dynamo-browse/models/queryexpr" "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/tables" "github.com/pkg/errors" + "time" "ucl.lmika.dev/repl" "ucl.lmika.dev/ucl" ) @@ -19,11 +20,36 @@ type rsModule struct { var rsNewDoc = repl.Doc{ Brief: "Creates a new, empty result set", + Usage: "[-table NAME]", + Detailed: ` + The result set assumes the details of the current table. If no table is specified, + the command will return an error. + `, } -func (rs *rsModule) rsNew(ctx context.Context, args ucl.CallArgs) (any, error) { - return &ResultSetProxy{ - RS: &models.ResultSet{}, +func (rs *rsModule) rsNew(ctx context.Context, args ucl.CallArgs) (_ any, err error) { + var tableInfo *models.TableInfo + if args.HasSwitch("table") { + var tblName string + if err := args.BindSwitch("table", &tblName); err != nil { + return nil, err + } + + tableInfo, err = rs.tableService.Describe(ctx, tblName) + if err != nil { + return nil, err + } + } else if currRs := rs.state.ResultSet(); currRs != nil && currRs.TableInfo != nil { + tableInfo = currRs.TableInfo + } else { + return nil, errors.New("no table specified") + } + + return ResultSetProxy{ + RS: &models.ResultSet{ + TableInfo: tableInfo, + Created: time.Now(), + }, }, nil } @@ -102,7 +128,7 @@ func (rs *rsModule) rsQuery(ctx context.Context, args ucl.CallArgs) (any, error) return nil, err } - return &ResultSetProxy{ + return ResultSetProxy{ RS: newResultSet, }, nil } diff --git a/internal/common/ui/commandctrl/cmdpacks/pvars.go b/internal/common/ui/commandctrl/cmdpacks/pvars.go new file mode 100644 index 0000000..5f5020e --- /dev/null +++ b/internal/common/ui/commandctrl/cmdpacks/pvars.go @@ -0,0 +1,31 @@ +package cmdpacks + +import ( + "context" + "github.com/lmika/dynamo-browse/internal/common/ui/commandctrl" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/controllers" + "github.com/pkg/errors" + "log" +) + +type resultSetPVar struct { + state *controllers.State + readController *controllers.TableReadController +} + +func (rs resultSetPVar) Get(ctx context.Context) (any, error) { + return ResultSetProxy{rs.state.ResultSet()}, nil +} + +func (rs resultSetPVar) Set(ctx context.Context, value any) error { + rsVal, ok := value.(ResultSetProxy) + if !ok { + return errors.New("new value to @resultset is not a result set") + } + + log.Printf("type = %T", rsVal.RS) + + msg := rs.readController.SetResultSet(rsVal.RS) + commandctrl.PostMsg(ctx, msg) + return nil +} diff --git a/internal/common/ui/commandctrl/cmdpacks/stdcmds.go b/internal/common/ui/commandctrl/cmdpacks/stdcmds.go index 21756a3..4682926 100644 --- a/internal/common/ui/commandctrl/cmdpacks/stdcmds.go +++ b/internal/common/ui/commandctrl/cmdpacks/stdcmds.go @@ -384,4 +384,6 @@ func (sc StandardCommands) ConfigureUCL(ucl *ucl.Inst) { ucl.SetBuiltin("noisy-touch", sc.cmdNoisyTouch) ucl.SetBuiltin("rebind", sc.cmdRebind) // set-opt --> alias to opts:set + + ucl.SetPseudoVar("resultset", resultSetPVar{sc.State, sc.ReadController}) } diff --git a/internal/dynamo-browse/controllers/state.go b/internal/dynamo-browse/controllers/state.go index 6a886d2..8f7ea82 100644 --- a/internal/dynamo-browse/controllers/state.go +++ b/internal/dynamo-browse/controllers/state.go @@ -29,6 +29,14 @@ func (s *State) Filter() string { return s.filter } +func (s *State) SetResultSet(resultSet *models.ResultSet) { + s.mutex.Lock() + defer s.mutex.Unlock() + + s.resultSet = resultSet + s.filter = "" +} + func (s *State) withResultSet(rs func(*models.ResultSet)) { s.mutex.Lock() defer s.mutex.Unlock() diff --git a/internal/dynamo-browse/controllers/tableread.go b/internal/dynamo-browse/controllers/tableread.go index 6ef6e18..e7034a5 100644 --- a/internal/dynamo-browse/controllers/tableread.go +++ b/internal/dynamo-browse/controllers/tableread.go @@ -291,6 +291,12 @@ func (c *TableReadController) setResultSetAndFilter(resultSet *models.ResultSet, return c.state.buildNewResultSetMessage("") } +func (c *TableReadController) SetResultSet(resultSet *models.ResultSet) tea.Msg { + c.state.setResultSetAndFilter(resultSet, "") + c.eventBus.Fire(newResultSetEvent, resultSet, resultSetUpdateScript) + return c.state.buildNewResultSetMessage("") +} + func (c *TableReadController) Mark(op MarkOp, where string) tea.Msg { var ( whereExpr *queryexpr.QueryExpr diff --git a/test/testdynamo/client.go b/test/testdynamo/client.go index 68c9065..688ea25 100644 --- a/test/testdynamo/client.go +++ b/test/testdynamo/client.go @@ -2,6 +2,7 @@ package testdynamo import ( "context" + "os" "testing" "github.com/aws/aws-sdk-go-v2/aws" @@ -28,8 +29,13 @@ func SetupTestTable(t *testing.T, testData []TestData) *dynamodb.Client { config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider("abc", "123", ""))) assert.NoError(t, err) + testDynamoURL, ok := os.LookupEnv("TEST_DYNAMO_URL") + if !ok { + testDynamoURL = "http://localhost:4566" + } + dynamoClient := dynamodb.NewFromConfig(cfg, - dynamodb.WithEndpointResolver(dynamodb.EndpointResolverFromURL("http://localhost:4566"))) + dynamodb.WithEndpointResolver(dynamodb.EndpointResolverFromURL(testDynamoURL))) for _, table := range testData { tableInput := &dynamodb.CreateTableInput{ From 40f8dd76e230ce2b644dffd13074aba117177e67 Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Sun, 18 May 2025 13:42:44 +1000 Subject: [PATCH 08/13] ucl: Have started adding some of the psudo variables --- go.mod | 2 +- go.sum | 10 + .../common/ui/commandctrl/cmdpacks/modrs.go | 14 +- .../common/ui/commandctrl/cmdpacks/proxy.go | 179 +++++++++++++++++- .../common/ui/commandctrl/cmdpacks/pvars.go | 43 ++++- .../common/ui/commandctrl/cmdpacks/stdcmds.go | 2 + internal/common/ui/commandctrl/ctx.go | 9 + internal/common/ui/commandctrl/iface.go | 2 + internal/dynamo-browse/ui/model.go | 4 + .../ui/teamodels/dynamotableview/model.go | 21 ++ 10 files changed, 267 insertions(+), 19 deletions(-) diff --git a/go.mod b/go.mod index 51c142d..887aaf1 100644 --- a/go.mod +++ b/go.mod @@ -117,5 +117,5 @@ require ( golang.org/x/text v0.9.0 // indirect google.golang.org/appengine v1.6.7 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - ucl.lmika.dev v0.0.0-20250517115116-0f1ceba0902e // indirect + ucl.lmika.dev v0.0.0-20250518033831-f79e91e26d78 // indirect ) diff --git a/go.sum b/go.sum index 1e50447..d77e7b4 100644 --- a/go.sum +++ b/go.sum @@ -444,3 +444,13 @@ ucl.lmika.dev v0.0.0-20250517003439-109be33d1495 h1:r46r+7T59Drm+in7TEWKCZfFYIM0 ucl.lmika.dev v0.0.0-20250517003439-109be33d1495/go.mod h1:/MMZKm6mOMtnY4I8TYEot4Pc8dKEy+/IAQo1VdpA5EY= ucl.lmika.dev v0.0.0-20250517115116-0f1ceba0902e h1:CQ+qPqI5lYiiEM0tNAr4jS0iMz16bFqOui5mU3AHsCU= ucl.lmika.dev v0.0.0-20250517115116-0f1ceba0902e/go.mod h1:/MMZKm6mOMtnY4I8TYEot4Pc8dKEy+/IAQo1VdpA5EY= +ucl.lmika.dev v0.0.0-20250517212052-51e35aa9a675 h1:kGKh3zj6lMzOrGAquFW7ROgx9/6nwJ8DXiSLtceRiak= +ucl.lmika.dev v0.0.0-20250517212052-51e35aa9a675/go.mod h1:/MMZKm6mOMtnY4I8TYEot4Pc8dKEy+/IAQo1VdpA5EY= +ucl.lmika.dev v0.0.0-20250517212757-33d04ba18db4 h1:rnietWu2B+NXLqKfo7jgf6r+srMwxFa5eizywkq4LFk= +ucl.lmika.dev v0.0.0-20250517212757-33d04ba18db4/go.mod h1:/MMZKm6mOMtnY4I8TYEot4Pc8dKEy+/IAQo1VdpA5EY= +ucl.lmika.dev v0.0.0-20250517213937-94aad417121d h1:CMcA8aQV6iiPK75EbHvoIVZhZmSggfrWNhK9BFm2aIg= +ucl.lmika.dev v0.0.0-20250517213937-94aad417121d/go.mod h1:/MMZKm6mOMtnY4I8TYEot4Pc8dKEy+/IAQo1VdpA5EY= +ucl.lmika.dev v0.0.0-20250518024533-f4be44fcbc94 h1:x3IRtT1jbedblimi2hesKGBihg243+wNOSvagCPR0KU= +ucl.lmika.dev v0.0.0-20250518024533-f4be44fcbc94/go.mod h1:/MMZKm6mOMtnY4I8TYEot4Pc8dKEy+/IAQo1VdpA5EY= +ucl.lmika.dev v0.0.0-20250518033831-f79e91e26d78 h1:lbOZUb6whYMLI4win5QL+eLSgqc3N9TtTgT8hTipNl8= +ucl.lmika.dev v0.0.0-20250518033831-f79e91e26d78/go.mod h1:/MMZKm6mOMtnY4I8TYEot4Pc8dKEy+/IAQo1VdpA5EY= diff --git a/internal/common/ui/commandctrl/cmdpacks/modrs.go b/internal/common/ui/commandctrl/cmdpacks/modrs.go index 4af1513..5f3060b 100644 --- a/internal/common/ui/commandctrl/cmdpacks/modrs.go +++ b/internal/common/ui/commandctrl/cmdpacks/modrs.go @@ -45,12 +45,10 @@ func (rs *rsModule) rsNew(ctx context.Context, args ucl.CallArgs) (_ any, err er return nil, errors.New("no table specified") } - return ResultSetProxy{ - RS: &models.ResultSet{ - TableInfo: tableInfo, - Created: time.Now(), - }, - }, nil + return newResultSetProxy(&models.ResultSet{ + TableInfo: tableInfo, + Created: time.Now(), + }), nil } var rsQueryDoc = repl.Doc{ @@ -128,9 +126,7 @@ func (rs *rsModule) rsQuery(ctx context.Context, args ucl.CallArgs) (any, error) return nil, err } - return ResultSetProxy{ - RS: newResultSet, - }, nil + return newResultSetProxy(newResultSet), nil } func moduleRS(tableService *tables.Service, state *controllers.State) ucl.Module { diff --git a/internal/common/ui/commandctrl/cmdpacks/proxy.go b/internal/common/ui/commandctrl/cmdpacks/proxy.go index b80aab2..5606f94 100644 --- a/internal/common/ui/commandctrl/cmdpacks/proxy.go +++ b/internal/common/ui/commandctrl/cmdpacks/proxy.go @@ -1,7 +1,180 @@ package cmdpacks -import "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" +import ( + "fmt" + "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" + "maps" + "strconv" + "ucl.lmika.dev/ucl" +) -type ResultSetProxy struct { - RS *models.ResultSet +type proxyFields[T any] map[string]func(t T) ucl.Object + +type simpleProxy[T comparable] struct { + value T + fields proxyFields[T] +} + +func (tp simpleProxy[T]) String() string { + return fmt.Sprint(tp.value) +} + +func (tp simpleProxy[T]) Truthy() bool { + var zeroT T + return tp.value != zeroT +} + +func (tp simpleProxy[T]) Len() int { + return len(tp.fields) +} + +func (tp simpleProxy[T]) Value(k string) ucl.Object { + f, ok := tp.fields[k] + if !ok { + return nil + } + return f(tp.value) +} + +func (tp simpleProxy[T]) Each(fn func(k string, v ucl.Object) error) error { + for key := range maps.Keys(tp.fields) { + if err := fn(key, tp.Value(key)); err != nil { + return err + } + } + return nil +} + +type simpleProxyList[T comparable] struct { + values []T + converter func(T) ucl.Object +} + +func newSimpleProxyList[T comparable](values []T, converter func(T) ucl.Object) simpleProxyList[T] { + return simpleProxyList[T]{values: values, converter: converter} +} + +func (tp simpleProxyList[T]) String() string { + return fmt.Sprint(tp.values) +} + +func (tp simpleProxyList[T]) Truthy() bool { + return len(tp.values) > 0 +} + +func (tp simpleProxyList[T]) Len() int { + return len(tp.values) +} + +func (tp simpleProxyList[T]) Index(k int) ucl.Object { + return tp.converter(tp.values[k]) +} + +func newResultSetProxy(rs *models.ResultSet) ucl.Object { + return simpleProxy[*models.ResultSet]{value: rs, fields: resultSetProxyFields} +} + +var resultSetProxyFields = proxyFields[*models.ResultSet]{ + "Table": func(t *models.ResultSet) ucl.Object { return newTableProxy(t.TableInfo) }, + "Items": func(t *models.ResultSet) ucl.Object { return resultSetItemsProxy{t} }, +} + +func newTableProxy(table *models.TableInfo) ucl.Object { + return simpleProxy[*models.TableInfo]{value: table, fields: tableProxyFields} +} + +var tableProxyFields = proxyFields[*models.TableInfo]{ + "Name": func(t *models.TableInfo) ucl.Object { return ucl.StringObject(t.Name) }, + "Keys": func(t *models.TableInfo) ucl.Object { return newKeyAttributeProxy(t.Keys) }, + "DefinedAttributes": func(t *models.TableInfo) ucl.Object { return ucl.StringListObject(t.DefinedAttributes) }, + "GSIs": func(t *models.TableInfo) ucl.Object { return newSimpleProxyList(t.GSIs, newGSIProxy) }, +} + +func newKeyAttributeProxy(keyAttrs models.KeyAttribute) ucl.Object { + return simpleProxy[models.KeyAttribute]{value: keyAttrs, fields: keyAttributeProxyFields} +} + +var keyAttributeProxyFields = proxyFields[models.KeyAttribute]{ + "PartitionKey": func(t models.KeyAttribute) ucl.Object { return ucl.StringObject(t.PartitionKey) }, + "SortKey": func(t models.KeyAttribute) ucl.Object { return ucl.StringObject(t.SortKey) }, +} + +func newGSIProxy(gsi models.TableGSI) ucl.Object { + return simpleProxy[models.TableGSI]{value: gsi, fields: gsiProxyFields} +} + +var gsiProxyFields = proxyFields[models.TableGSI]{ + "Name": func(t models.TableGSI) ucl.Object { return ucl.StringObject(t.Name) }, + "Keys": func(t models.TableGSI) ucl.Object { return newKeyAttributeProxy(t.Keys) }, +} + +type resultSetItemsProxy struct { + resultSet *models.ResultSet +} + +func (ip resultSetItemsProxy) String() string { + return "items" +} + +func (ip resultSetItemsProxy) Truthy() bool { + return len(ip.resultSet.Items()) > 0 +} + +func (tp resultSetItemsProxy) Len() int { + return len(tp.resultSet.Items()) +} + +func (tp resultSetItemsProxy) Index(k int) ucl.Object { + return itemProxy{resultSet: tp.resultSet, idx: k, item: tp.resultSet.Items()[k]} +} + +type itemProxy struct { + resultSet *models.ResultSet + idx int + item models.Item +} + +func (ip itemProxy) String() string { + return "item" +} + +func (ip itemProxy) Truthy() bool { + return len(ip.item) > 0 +} + +func (tp itemProxy) Len() int { + return len(tp.item) +} + +func (tp itemProxy) Value(k string) ucl.Object { + f, ok := tp.item[k] + if !ok { + return nil + } + return convertAttributeValueToUCLObject(f) +} + +func (tp itemProxy) Each(fn func(k string, v ucl.Object) error) error { + for key := range maps.Keys(tp.item) { + if err := fn(key, tp.Value(key)); err != nil { + return err + } + } + return nil +} + +func convertAttributeValueToUCLObject(attrValue types.AttributeValue) ucl.Object { + switch t := attrValue.(type) { + case *types.AttributeValueMemberS: + return ucl.StringObject(t.Value) + case *types.AttributeValueMemberN: + i, err := strconv.ParseInt(t.Value, 10, 64) + if err != nil { + return nil + } + return ucl.IntObject(i) + } + // TODO: the rest + return nil } diff --git a/internal/common/ui/commandctrl/cmdpacks/pvars.go b/internal/common/ui/commandctrl/cmdpacks/pvars.go index 5f5020e..d297a98 100644 --- a/internal/common/ui/commandctrl/cmdpacks/pvars.go +++ b/internal/common/ui/commandctrl/cmdpacks/pvars.go @@ -4,28 +4,59 @@ import ( "context" "github.com/lmika/dynamo-browse/internal/common/ui/commandctrl" "github.com/lmika/dynamo-browse/internal/dynamo-browse/controllers" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" "github.com/pkg/errors" - "log" ) +type tablePVar struct { + state *controllers.State +} + +func (rs tablePVar) Get(ctx context.Context) (any, error) { + return newTableProxy(rs.state.ResultSet().TableInfo), nil +} + type resultSetPVar struct { state *controllers.State readController *controllers.TableReadController } func (rs resultSetPVar) Get(ctx context.Context) (any, error) { - return ResultSetProxy{rs.state.ResultSet()}, nil + return newResultSetProxy(rs.state.ResultSet()), nil } func (rs resultSetPVar) Set(ctx context.Context, value any) error { - rsVal, ok := value.(ResultSetProxy) + rsVal, ok := value.(simpleProxy[*models.ResultSet]) if !ok { return errors.New("new value to @resultset is not a result set") } - log.Printf("type = %T", rsVal.RS) - - msg := rs.readController.SetResultSet(rsVal.RS) + msg := rs.readController.SetResultSet(rsVal.value) commandctrl.PostMsg(ctx, msg) return nil } + +type itemPVar struct { + state *controllers.State +} + +func (rs itemPVar) Get(ctx context.Context) (any, error) { + selItem, ok := commandctrl.SelectedItemIndex(ctx) + if !ok { + return nil, errors.New("no item selected") + } + return itemProxy{rs.state.ResultSet(), selItem, rs.state.ResultSet().Items()[selItem]}, nil +} + +func (rs itemPVar) Set(ctx context.Context, value any) error { + rsVal, ok := value.(itemProxy) + if !ok { + return errors.New("new value to @item is not an item") + } + + if msg := commandctrl.SetSelectedItemIndex(ctx, rsVal.idx); msg != nil { + commandctrl.PostMsg(ctx, msg) + } + + return nil +} diff --git a/internal/common/ui/commandctrl/cmdpacks/stdcmds.go b/internal/common/ui/commandctrl/cmdpacks/stdcmds.go index 4682926..e5c1aab 100644 --- a/internal/common/ui/commandctrl/cmdpacks/stdcmds.go +++ b/internal/common/ui/commandctrl/cmdpacks/stdcmds.go @@ -386,4 +386,6 @@ func (sc StandardCommands) ConfigureUCL(ucl *ucl.Inst) { // set-opt --> alias to opts:set ucl.SetPseudoVar("resultset", resultSetPVar{sc.State, sc.ReadController}) + ucl.SetPseudoVar("table", tablePVar{sc.State}) + ucl.SetPseudoVar("item", itemPVar{sc.State}) } diff --git a/internal/common/ui/commandctrl/ctx.go b/internal/common/ui/commandctrl/ctx.go index 7c6f193..7fc70ea 100644 --- a/internal/common/ui/commandctrl/ctx.go +++ b/internal/common/ui/commandctrl/ctx.go @@ -24,3 +24,12 @@ func SelectedItemIndex(ctx context.Context) (int, bool) { return cmdCtl.uiStateProvider.SelectedItemIndex(), true } + +func SetSelectedItemIndex(ctx context.Context, newIdx int) tea.Msg { + cmdCtl, ok := ctx.Value(commandCtlKey).(*CommandController) + if !ok { + return nil + } + + return cmdCtl.uiStateProvider.SetSelectedItemIndex(newIdx) +} diff --git a/internal/common/ui/commandctrl/iface.go b/internal/common/ui/commandctrl/iface.go index 41d3a7f..e708d24 100644 --- a/internal/common/ui/commandctrl/iface.go +++ b/internal/common/ui/commandctrl/iface.go @@ -2,6 +2,7 @@ package commandctrl import ( "context" + tea "github.com/charmbracelet/bubbletea" "github.com/lmika/dynamo-browse/internal/dynamo-browse/services" ) @@ -11,4 +12,5 @@ type IterProvider interface { type UIStateProvider interface { SelectedItemIndex() int + SetSelectedItemIndex(newIdx int) tea.Msg } diff --git a/internal/dynamo-browse/ui/model.go b/internal/dynamo-browse/ui/model.go index d57c816..9acb70e 100644 --- a/internal/dynamo-browse/ui/model.go +++ b/internal/dynamo-browse/ui/model.go @@ -397,3 +397,7 @@ func (m *Model) promptToQuit() tea.Msg { func (m *Model) SelectedItemIndex() int { return m.tableView.SelectedItemIndex() } + +func (m *Model) SetSelectedItemIndex(newIdx int) tea.Msg { + return m.tableView.SetSelectedItemIndex(newIdx) +} diff --git a/internal/dynamo-browse/ui/teamodels/dynamotableview/model.go b/internal/dynamo-browse/ui/teamodels/dynamotableview/model.go index cf61d72..163de6f 100644 --- a/internal/dynamo-browse/ui/teamodels/dynamotableview/model.go +++ b/internal/dynamo-browse/ui/teamodels/dynamotableview/model.go @@ -208,6 +208,27 @@ func (m *Model) SelectedItemIndex() int { return selectedItem.itemIndex } +func (m *Model) SetSelectedItemIndex(newIdx int) tea.Msg { + cursor := m.table.Cursor() + switch { + case newIdx <= 0: + m.table.GoTop() + case newIdx >= len(m.rows)-1: + m.table.GoBottom() + case newIdx < cursor: + delta := cursor - newIdx + for d := 0; d < delta; d++ { + m.table.GoUp() + } + case newIdx > cursor: + delta := newIdx - cursor + for d := 0; d < delta; d++ { + m.table.GoDown() + } + } + return m.postSelectedItemChanged() +} + func (m *Model) selectedItem() (itemTableRow, bool) { resultSet := m.resultSet From 50880096725609bfb8d1ed79131cb24ca4410d90 Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Mon, 19 May 2025 22:14:22 +1000 Subject: [PATCH 09/13] ucl: added more resultset functions --- go.mod | 2 +- go.sum | 6 + .../common/ui/commandctrl/cmdpacks/modrs.go | 85 +++++++++++- .../ui/commandctrl/cmdpacks/modrs_test.go | 80 ++++++++++- .../common/ui/commandctrl/cmdpacks/proxy.go | 102 +++++++++----- .../common/ui/commandctrl/cmdpacks/pvars.go | 4 +- .../ui/commandctrl/cmdpacks/stdcmds_test.go | 125 ++++++++++++++---- internal/dynamo-browse/models/models.go | 34 +++++ 8 files changed, 369 insertions(+), 69 deletions(-) diff --git a/go.mod b/go.mod index 887aaf1..ac57b4e 100644 --- a/go.mod +++ b/go.mod @@ -117,5 +117,5 @@ require ( golang.org/x/text v0.9.0 // indirect google.golang.org/appengine v1.6.7 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - ucl.lmika.dev v0.0.0-20250518033831-f79e91e26d78 // indirect + ucl.lmika.dev v0.0.0-20250519120409-53b05b5ba6f8 // indirect ) diff --git a/go.sum b/go.sum index d77e7b4..0aafeb1 100644 --- a/go.sum +++ b/go.sum @@ -454,3 +454,9 @@ ucl.lmika.dev v0.0.0-20250518024533-f4be44fcbc94 h1:x3IRtT1jbedblimi2hesKGBihg24 ucl.lmika.dev v0.0.0-20250518024533-f4be44fcbc94/go.mod h1:/MMZKm6mOMtnY4I8TYEot4Pc8dKEy+/IAQo1VdpA5EY= ucl.lmika.dev v0.0.0-20250518033831-f79e91e26d78 h1:lbOZUb6whYMLI4win5QL+eLSgqc3N9TtTgT8hTipNl8= ucl.lmika.dev v0.0.0-20250518033831-f79e91e26d78/go.mod h1:/MMZKm6mOMtnY4I8TYEot4Pc8dKEy+/IAQo1VdpA5EY= +ucl.lmika.dev v0.0.0-20250519111943-1173d163f5e3 h1:ZMQ1rkcAWa///c3bVvlXbtuqjfAWxDm01abQl3g/YVw= +ucl.lmika.dev v0.0.0-20250519111943-1173d163f5e3/go.mod h1:/MMZKm6mOMtnY4I8TYEot4Pc8dKEy+/IAQo1VdpA5EY= +ucl.lmika.dev v0.0.0-20250519114239-7ca821016e9a h1:dzBBFCY50+MQcJaQ90swdDyjzag5oIhwdfqbmZkvX3Q= +ucl.lmika.dev v0.0.0-20250519114239-7ca821016e9a/go.mod h1:/MMZKm6mOMtnY4I8TYEot4Pc8dKEy+/IAQo1VdpA5EY= +ucl.lmika.dev v0.0.0-20250519120409-53b05b5ba6f8 h1:h32JQi0d1MI86RaAMaEU7kvti4uSLX5XYe/nk2abApg= +ucl.lmika.dev v0.0.0-20250519120409-53b05b5ba6f8/go.mod h1:/MMZKm6mOMtnY4I8TYEot4Pc8dKEy+/IAQo1VdpA5EY= diff --git a/internal/common/ui/commandctrl/cmdpacks/modrs.go b/internal/common/ui/commandctrl/cmdpacks/modrs.go index 5f3060b..eea66b4 100644 --- a/internal/common/ui/commandctrl/cmdpacks/modrs.go +++ b/internal/common/ui/commandctrl/cmdpacks/modrs.go @@ -129,6 +129,84 @@ func (rs *rsModule) rsQuery(ctx context.Context, args ucl.CallArgs) (any, error) return newResultSetProxy(newResultSet), nil } +var rsScanDoc = repl.Doc{ + Brief: "Performs a scan of the table and returns the results as a result-set", + Usage: "[-table NAME]", + Args: []repl.ArgDoc{ + {Name: "-table", Brief: "Optional table name to use for the query"}, + }, + Detailed: ` + If no table is specified, then the value of @table will be used. If this is unavailable, + the command will return an error. + `, +} + +func (rs *rsModule) rsScan(ctx context.Context, args ucl.CallArgs) (_ any, err error) { + var tableInfo *models.TableInfo + if args.HasSwitch("table") { + var tblName string + if err := args.BindSwitch("table", &tblName); err != nil { + return nil, err + } + + tableInfo, err = rs.tableService.Describe(ctx, tblName) + if err != nil { + return nil, err + } + } else if currRs := rs.state.ResultSet(); currRs != nil && currRs.TableInfo != nil { + tableInfo = currRs.TableInfo + } else { + return nil, errors.New("no table specified") + } + + newResultSet, err := rs.tableService.Scan(context.Background(), tableInfo) + if err != nil { + return nil, err + } + + return newResultSetProxy(newResultSet), nil +} + +var rsNextPageDoc = repl.Doc{ + Brief: "Returns the next page of the passed in result-set", + Usage: "RESULT_SET", + Args: []repl.ArgDoc{ + {Name: "result-set", Brief: "Result set to fetch the next page of"}, + }, + Detailed: ` + If no next page exists, the command will return nil. + `, +} + +func (rs *rsModule) rsNextPage(ctx context.Context, args ucl.CallArgs) (_ any, err error) { + var rsProxy SimpleProxy[*models.ResultSet] + + if err := args.Bind(&rsProxy); err != nil { + return nil, err + } + + if !rsProxy.value.HasNextPage() { + return nil, nil + } + + nextPage, err := rs.tableService.NextPage(ctx, rsProxy.value) + if err != nil { + return nil, err + } + + return newResultSetProxy(nextPage), nil +} + +func (rs *rsModule) rsUnion(ctx context.Context, args ucl.CallArgs) (_ any, err error) { + var rsProxy1, rsProxy2 SimpleProxy[*models.ResultSet] + + if err := args.Bind(&rsProxy1, &rsProxy2); err != nil { + return nil, err + } + + return newResultSetProxy(rsProxy1.ProxyValue().MergeWith(rsProxy2.ProxyValue())), nil +} + func moduleRS(tableService *tables.Service, state *controllers.State) ucl.Module { m := &rsModule{ tableService: tableService, @@ -138,8 +216,11 @@ func moduleRS(tableService *tables.Service, state *controllers.State) ucl.Module return ucl.Module{ Name: "rs", Builtins: map[string]ucl.BuiltinHandler{ - "new": m.rsNew, - "query": m.rsQuery, + "new": m.rsNew, + "query": m.rsQuery, + "scan": m.rsScan, + "next-page": m.rsNextPage, + "union": m.rsUnion, }, } } diff --git a/internal/common/ui/commandctrl/cmdpacks/modrs_test.go b/internal/common/ui/commandctrl/cmdpacks/modrs_test.go index 113bd46..878ad3b 100644 --- a/internal/common/ui/commandctrl/cmdpacks/modrs_test.go +++ b/internal/common/ui/commandctrl/cmdpacks/modrs_test.go @@ -3,6 +3,7 @@ package cmdpacks_test import ( "fmt" "github.com/lmika/dynamo-browse/internal/common/ui/commandctrl/cmdpacks" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" "github.com/stretchr/testify/assert" "testing" ) @@ -13,7 +14,80 @@ func TestModRS_New(t *testing.T) { rsProxy, err := svc.CommandController.ExecuteAndWait(t.Context(), `rs:new`) assert.NoError(t, err) - assert.IsType(t, rsProxy, &cmdpacks.ResultSetProxy{}) + assert.IsType(t, rsProxy, cmdpacks.SimpleProxy[*models.ResultSet]{}) +} + +func TestModRS_NextPage(t *testing.T) { + t.Run("multiple pages", func(t *testing.T) { + svc := newService(t, withDataGenerator(largeTestData), withTable("large-table"), withDefaultLimit(20)) + + hasNextPage, err := svc.CommandController.ExecuteAndWait(t.Context(), `@resultset.HasNextPage`) + assert.NoError(t, err) + assert.True(t, hasNextPage.(bool)) + + // Page 2 + rsProxy, err := svc.CommandController.ExecuteAndWait(t.Context(), `rs:next-page @resultset`) + + assert.NoError(t, err) + assert.IsType(t, cmdpacks.SimpleProxy[*models.ResultSet]{}, rsProxy) + assert.Equal(t, 20, len(rsProxy.(cmdpacks.SimpleProxy[*models.ResultSet]).ProxyValue().Items())) + + hasNextPage, err = svc.CommandController.ExecuteAndWait(t.Context(), `(rs:next-page @resultset).HasNextPage`) + assert.NoError(t, err) + assert.True(t, hasNextPage.(bool)) + + // Page 3 + rsProxy, err = svc.CommandController.ExecuteAndWait(t.Context(), `rs:next-page @resultset | rs:next-page`) + + assert.NoError(t, err) + assert.IsType(t, cmdpacks.SimpleProxy[*models.ResultSet]{}, rsProxy) + assert.Equal(t, 10, len(rsProxy.(cmdpacks.SimpleProxy[*models.ResultSet]).ProxyValue().Items())) + + hasNextPage, err = svc.CommandController.ExecuteAndWait(t.Context(), `(rs:next-page @resultset | rs:next-page).HasNextPage`) + assert.NoError(t, err) + assert.False(t, hasNextPage.(bool)) + + // Last page + rsProxy, err = svc.CommandController.ExecuteAndWait(t.Context(), `rs:next-page (rs:next-page @resultset | rs:next-page)`) + assert.NoError(t, err) + assert.Nil(t, rsProxy) + }) + + t.Run("only one page", func(t *testing.T) { + svc := newService(t) + + hasNextPage, err := svc.CommandController.ExecuteAndWait(t.Context(), `@resultset.HasNextPage`) + assert.NoError(t, err) + assert.False(t, hasNextPage.(bool)) + + rsProxy, err := svc.CommandController.ExecuteAndWait(t.Context(), `rs:next-page @resultset`) + assert.NoError(t, err) + assert.Nil(t, rsProxy) + }) +} + +func TestModRS_Union(t *testing.T) { + svc := newService(t, withDefaultLimit(2)) + + rsProxy, err := svc.CommandController.ExecuteAndWait(t.Context(), ` + $mr = rs:union @resultset (rs:next-page @resultset) + + assert (eq (len $mr.Items) 3) "expected len == 3" + assert (eq $mr.Items.(0).pk "abc") "expected 0.pk" + assert (eq $mr.Items.(0).sk "111") "expected 0.sk" + assert (eq $mr.Items.(1).pk "abc") "expected 1.pk" + assert (eq $mr.Items.(1).sk "222") "expected 1.sk" + assert (eq $mr.Items.(2).pk "bbb") "expected 2.pk" + assert (eq $mr.Items.(2).sk "131") "expected 2.sk" + + $mr + `) + + assert.NoError(t, err) + assert.IsType(t, cmdpacks.SimpleProxy[*models.ResultSet]{}, rsProxy) + + rs := rsProxy.(cmdpacks.SimpleProxy[*models.ResultSet]).ProxyValue() + assert.Equal(t, 3, len(rs.Items())) } func TestModRS_Query(t *testing.T) { @@ -65,11 +139,11 @@ func TestModRS_Query(t *testing.T) { res, err := svc.CommandController.ExecuteAndWait(t.Context(), tt.cmd) assert.NoError(t, err) - rs := res.(*cmdpacks.ResultSetProxy).RS + rs := res.(cmdpacks.SimpleProxy[*models.ResultSet]).ProxyValue() assert.Len(t, rs.Items(), len(tt.wantRows)) for i, rowIndex := range tt.wantRows { - for key, want := range testData[0].Data[rowIndex] { + for key, want := range svc.testData[0].Data[rowIndex] { have, ok := rs.Items()[i].AttributeValueAsString(key) assert.True(t, ok) assert.Equal(t, fmt.Sprint(want), have) diff --git a/internal/common/ui/commandctrl/cmdpacks/proxy.go b/internal/common/ui/commandctrl/cmdpacks/proxy.go index 5606f94..6801946 100644 --- a/internal/common/ui/commandctrl/cmdpacks/proxy.go +++ b/internal/common/ui/commandctrl/cmdpacks/proxy.go @@ -9,36 +9,50 @@ import ( "ucl.lmika.dev/ucl" ) -type proxyFields[T any] map[string]func(t T) ucl.Object - -type simpleProxy[T comparable] struct { - value T - fields proxyFields[T] +type proxyInfo[T comparable] struct { + fields map[string]func(t T) ucl.Object + lenFunc func(t T) int + strFunc func(t T) string } -func (tp simpleProxy[T]) String() string { +type SimpleProxy[T comparable] struct { + value T + proxyInfo *proxyInfo[T] +} + +func (tp SimpleProxy[T]) ProxyValue() T { + return tp.value +} + +func (tp SimpleProxy[T]) String() string { + if tp.proxyInfo.strFunc != nil { + return tp.proxyInfo.strFunc(tp.value) + } return fmt.Sprint(tp.value) } -func (tp simpleProxy[T]) Truthy() bool { +func (tp SimpleProxy[T]) Truthy() bool { var zeroT T return tp.value != zeroT } -func (tp simpleProxy[T]) Len() int { - return len(tp.fields) +func (tp SimpleProxy[T]) Len() int { + if tp.proxyInfo.lenFunc != nil { + return tp.proxyInfo.lenFunc(tp.value) + } + return len(tp.proxyInfo.fields) } -func (tp simpleProxy[T]) Value(k string) ucl.Object { - f, ok := tp.fields[k] +func (tp SimpleProxy[T]) Value(k string) ucl.Object { + f, ok := tp.proxyInfo.fields[k] if !ok { return nil } return f(tp.value) } -func (tp simpleProxy[T]) Each(fn func(k string, v ucl.Object) error) error { - for key := range maps.Keys(tp.fields) { +func (tp SimpleProxy[T]) Each(fn func(k string, v ucl.Object) error) error { + for key := range maps.Keys(tp.proxyInfo.fields) { if err := fn(key, tp.Value(key)); err != nil { return err } @@ -72,41 +86,63 @@ func (tp simpleProxyList[T]) Index(k int) ucl.Object { } func newResultSetProxy(rs *models.ResultSet) ucl.Object { - return simpleProxy[*models.ResultSet]{value: rs, fields: resultSetProxyFields} + return SimpleProxy[*models.ResultSet]{value: rs, proxyInfo: resultSetProxyFields} } -var resultSetProxyFields = proxyFields[*models.ResultSet]{ - "Table": func(t *models.ResultSet) ucl.Object { return newTableProxy(t.TableInfo) }, - "Items": func(t *models.ResultSet) ucl.Object { return resultSetItemsProxy{t} }, +var resultSetProxyFields = &proxyInfo[*models.ResultSet]{ + lenFunc: func(t *models.ResultSet) int { return len(t.Items()) }, + strFunc: func(t *models.ResultSet) string { + return fmt.Sprintf("ResultSet(%v:%d)", t.TableInfo.Name, len(t.Items())) + }, + fields: map[string]func(t *models.ResultSet) ucl.Object{ + "Table": func(t *models.ResultSet) ucl.Object { return newTableProxy(t.TableInfo) }, + "Items": func(t *models.ResultSet) ucl.Object { return resultSetItemsProxy{t} }, + "HasNextPage": func(t *models.ResultSet) ucl.Object { return ucl.BoolObject(t.HasNextPage()) }, + }, } func newTableProxy(table *models.TableInfo) ucl.Object { - return simpleProxy[*models.TableInfo]{value: table, fields: tableProxyFields} + return SimpleProxy[*models.TableInfo]{value: table, proxyInfo: tableProxyFields} } -var tableProxyFields = proxyFields[*models.TableInfo]{ - "Name": func(t *models.TableInfo) ucl.Object { return ucl.StringObject(t.Name) }, - "Keys": func(t *models.TableInfo) ucl.Object { return newKeyAttributeProxy(t.Keys) }, - "DefinedAttributes": func(t *models.TableInfo) ucl.Object { return ucl.StringListObject(t.DefinedAttributes) }, - "GSIs": func(t *models.TableInfo) ucl.Object { return newSimpleProxyList(t.GSIs, newGSIProxy) }, +var tableProxyFields = &proxyInfo[*models.TableInfo]{ + strFunc: func(t *models.TableInfo) string { + return fmt.Sprintf("Table(%v)", t.Name) + }, + fields: map[string]func(t *models.TableInfo) ucl.Object{ + "Name": func(t *models.TableInfo) ucl.Object { return ucl.StringObject(t.Name) }, + "Keys": func(t *models.TableInfo) ucl.Object { return newKeyAttributeProxy(t.Keys) }, + "DefinedAttributes": func(t *models.TableInfo) ucl.Object { return ucl.StringListObject(t.DefinedAttributes) }, + "GSIs": func(t *models.TableInfo) ucl.Object { return newSimpleProxyList(t.GSIs, newGSIProxy) }, + }, } func newKeyAttributeProxy(keyAttrs models.KeyAttribute) ucl.Object { - return simpleProxy[models.KeyAttribute]{value: keyAttrs, fields: keyAttributeProxyFields} + return SimpleProxy[models.KeyAttribute]{value: keyAttrs, proxyInfo: keyAttributeProxyFields} } -var keyAttributeProxyFields = proxyFields[models.KeyAttribute]{ - "PartitionKey": func(t models.KeyAttribute) ucl.Object { return ucl.StringObject(t.PartitionKey) }, - "SortKey": func(t models.KeyAttribute) ucl.Object { return ucl.StringObject(t.SortKey) }, +var keyAttributeProxyFields = &proxyInfo[models.KeyAttribute]{ + strFunc: func(t models.KeyAttribute) string { + return fmt.Sprintf("KeyAttribute(%v,%v)", t.PartitionKey, t.SortKey) + }, + fields: map[string]func(t models.KeyAttribute) ucl.Object{ + "PartitionKey": func(t models.KeyAttribute) ucl.Object { return ucl.StringObject(t.PartitionKey) }, + "SortKey": func(t models.KeyAttribute) ucl.Object { return ucl.StringObject(t.SortKey) }, + }, } func newGSIProxy(gsi models.TableGSI) ucl.Object { - return simpleProxy[models.TableGSI]{value: gsi, fields: gsiProxyFields} + return SimpleProxy[models.TableGSI]{value: gsi, proxyInfo: gsiProxyFields} } -var gsiProxyFields = proxyFields[models.TableGSI]{ - "Name": func(t models.TableGSI) ucl.Object { return ucl.StringObject(t.Name) }, - "Keys": func(t models.TableGSI) ucl.Object { return newKeyAttributeProxy(t.Keys) }, +var gsiProxyFields = &proxyInfo[models.TableGSI]{ + strFunc: func(t models.TableGSI) string { + return fmt.Sprintf("TableGSI(%v,(%v,%v))", t.Name, t.Keys.PartitionKey, t.Keys.SortKey) + }, + fields: map[string]func(t models.TableGSI) ucl.Object{ + "Name": func(t models.TableGSI) ucl.Object { return ucl.StringObject(t.Name) }, + "Keys": func(t models.TableGSI) ucl.Object { return newKeyAttributeProxy(t.Keys) }, + }, } type resultSetItemsProxy struct { @@ -114,7 +150,7 @@ type resultSetItemsProxy struct { } func (ip resultSetItemsProxy) String() string { - return "items" + return "RSItem()" } func (ip resultSetItemsProxy) Truthy() bool { @@ -136,7 +172,7 @@ type itemProxy struct { } func (ip itemProxy) String() string { - return "item" + return fmt.Sprintf("RSItems(%v)", len(ip.item)) } func (ip itemProxy) Truthy() bool { diff --git a/internal/common/ui/commandctrl/cmdpacks/pvars.go b/internal/common/ui/commandctrl/cmdpacks/pvars.go index d297a98..f07965a 100644 --- a/internal/common/ui/commandctrl/cmdpacks/pvars.go +++ b/internal/common/ui/commandctrl/cmdpacks/pvars.go @@ -26,9 +26,9 @@ func (rs resultSetPVar) Get(ctx context.Context) (any, error) { } func (rs resultSetPVar) Set(ctx context.Context, value any) error { - rsVal, ok := value.(simpleProxy[*models.ResultSet]) + rsVal, ok := value.(SimpleProxy[*models.ResultSet]) if !ok { - return errors.New("new value to @resultset is not a result set") + return errors.New("new value to @resultset is nil or not a result set") } msg := rs.readController.SetResultSet(rsVal.value) diff --git a/internal/common/ui/commandctrl/cmdpacks/stdcmds_test.go b/internal/common/ui/commandctrl/cmdpacks/stdcmds_test.go index 48dd158..44aff3e 100644 --- a/internal/common/ui/commandctrl/cmdpacks/stdcmds_test.go +++ b/internal/common/ui/commandctrl/cmdpacks/stdcmds_test.go @@ -1,6 +1,8 @@ package cmdpacks_test import ( + "fmt" + tea "github.com/charmbracelet/bubbletea" "github.com/lmika/dynamo-browse/internal/common/ui/commandctrl" "github.com/lmika/dynamo-browse/internal/common/ui/commandctrl/cmdpacks" "github.com/lmika/dynamo-browse/internal/dynamo-browse/controllers" @@ -48,14 +50,41 @@ func TestStdCmds_Mark(t *testing.T) { } } +type testDataGenerator func() []testdynamo.TestData type services struct { CommandController *commandctrl.CommandController SelItemIndex int State *controllers.State + + settingStore *settingstore.SettingStore + table string + + testDataGenerator testDataGenerator + testData []testdynamo.TestData } -func newService(t *testing.T) *services { +type serviceOpt func(*services) + +func withDataGenerator(tg testDataGenerator) serviceOpt { + return func(s *services) { + s.testDataGenerator = tg + } +} + +func withTable(table string) serviceOpt { + return func(s *services) { + s.table = table + } +} + +func withDefaultLimit(limit int) serviceOpt { + return func(s *services) { + s.settingStore.SetDefaultLimit(limit) + } +} + +func newService(t *testing.T, opts ...serviceOpt) *services { ws := testworkspace.New(t) resultSetSnapshotStore := workspacestore.NewResultSetSnapshotStore(ws) @@ -66,7 +95,18 @@ func newService(t *testing.T) *services { itemRendererService := itemrenderer.NewService(itemrenderer.PlainTextRenderer(), itemrenderer.PlainTextRenderer()) inputHistoryService := inputhistory.New(inputHistoryStore) - client := testdynamo.SetupTestTable(t, testData) + s := &services{ + table: "service-test-data", + settingStore: settingStore, + testDataGenerator: normalTestData, + } + + for _, opt := range opts { + opt(s) + } + + s.testData = s.testDataGenerator() + client := testdynamo.SetupTestTable(t, s.testData) provider := dynamo.NewProvider(client) service := tables.NewService(provider, settingStore) @@ -74,6 +114,7 @@ func newService(t *testing.T) *services { state := controllers.NewState() jobsController := controllers.NewJobsController(jobs.NewService(eventBus), eventBus, true) + readController := controllers.NewTableReadController( state, service, @@ -84,7 +125,7 @@ func newService(t *testing.T) *services { eventBus, pasteboardprovider.NilProvider{}, nil, - "service-test-data", + s.table, ) writeController := controllers.NewTableWriteController(state, service, jobsController, readController, settingStore) settingsController := controllers.NewSettingsController(settingStore, eventBus) @@ -102,10 +143,8 @@ func newService(t *testing.T) *services { }, ) - s := &services{ - State: state, - CommandController: commandController, - } + s.State = state + s.CommandController = commandController commandController.SetUIStateProvider(s) readController.Init() @@ -117,27 +156,57 @@ func (s *services) SelectedItemIndex() int { return s.SelItemIndex } -var testData = []testdynamo.TestData{ - { - TableName: "service-test-data", - Data: []map[string]interface{}{ - { - "pk": "abc", - "sk": "111", - "alpha": "This is some value", - }, - { - "pk": "abc", - "sk": "222", - "alpha": "This is another some value", - "beta": 1231, - }, - { - "pk": "bbb", - "sk": "131", - "beta": 2468, - "gamma": "foobar", +func (s *services) SetSelectedItemIndex(newIdx int) tea.Msg { + s.SelItemIndex = newIdx + return nil +} + +func normalTestData() []testdynamo.TestData { + return []testdynamo.TestData{ + { + TableName: "service-test-data", + Data: []map[string]interface{}{ + { + "pk": "abc", + "sk": "111", + "alpha": "This is some value", + }, + { + "pk": "abc", + "sk": "222", + "alpha": "This is another some value", + "beta": 1231, + }, + { + "pk": "bbb", + "sk": "131", + "beta": 2468, + "gamma": "foobar", + }, }, }, - }, + } +} + +func largeTestData() []testdynamo.TestData { + return []testdynamo.TestData{ + { + TableName: "large-table", + Data: genRow(50, func(i int) map[string]interface{} { + return map[string]interface{}{ + "pk": fmt.Sprint(i), + "sk": fmt.Sprint(i), + "alpha": fmt.Sprintf("row %v", i), + } + }), + }, + } +} + +func genRow(count int, mapFn func(int) map[string]interface{}) []map[string]interface{} { + result := make([]map[string]interface{}, count) + for i := 0; i < count; i++ { + result[i] = mapFn(i) + } + return result } diff --git a/internal/dynamo-browse/models/models.go b/internal/dynamo-browse/models/models.go index 03d0421..ff4e398 100644 --- a/internal/dynamo-browse/models/models.go +++ b/internal/dynamo-browse/models/models.go @@ -151,3 +151,37 @@ func (rs *ResultSet) Sort(criteria SortCriteria) { rs.sortCriteria = criteria Sort(rs.items, criteria) } + +func (rs *ResultSet) MergeWith(otherRS *ResultSet) *ResultSet { + type pksk struct { + pk types.AttributeValue + sk types.AttributeValue + } + + if !rs.TableInfo.Equal(otherRS.TableInfo) { + return nil + } + + itemsInI := make(map[pksk]Item) + newItems := make([]Item, 0, len(rs.Items())+len(otherRS.Items())) + for _, item := range rs.Items() { + pk, sk := item.PKSK(rs.TableInfo) + itemsInI[pksk{pk, sk}] = item + newItems = append(newItems, item) + } + + for _, item := range otherRS.Items() { + pk, sk := item.PKSK(rs.TableInfo) + if _, hasItem := itemsInI[pksk{pk, sk}]; !hasItem { + newItems = append(newItems, item) + } + } + + newResultSet := &ResultSet{ + Created: time.Now(), + TableInfo: rs.TableInfo, + } + newResultSet.SetItems(newItems) + + return newResultSet +} From 7ae99b009bd41ba059906d87d4dda76d04828e20 Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Fri, 23 May 2025 22:04:41 +1000 Subject: [PATCH 10/13] ucl: added rs:set --- cmd/dynamo-browse/main.go | 20 +- .../common/ui/commandctrl/cmdpacks/modrs.go | 78 ++++++- .../common/ui/commandctrl/cmdpacks/modui.go | 195 ++++++++++++++++++ .../common/ui/commandctrl/cmdpacks/stdcmds.go | 25 +++ .../ui/commandctrl/cmdpacks/stdcmds_test.go | 13 +- internal/common/ui/commandctrl/commandctrl.go | 23 ++- internal/common/ui/commandctrl/ctx.go | 39 +++- internal/common/ui/events/resultset.go | 5 + internal/dynamo-browse/controllers/events.go | 8 - .../dynamo-browse/controllers/keybinding.go | 4 + .../dynamo-browse/controllers/tableread.go | 30 ++- .../dynamo-browse/controllers/tablewrite.go | 20 +- internal/dynamo-browse/ui/model.go | 6 +- 13 files changed, 400 insertions(+), 66 deletions(-) create mode 100644 internal/common/ui/commandctrl/cmdpacks/modui.go create mode 100644 internal/common/ui/events/resultset.go diff --git a/cmd/dynamo-browse/main.go b/cmd/dynamo-browse/main.go index aeaf4eb..862941d 100644 --- a/cmd/dynamo-browse/main.go +++ b/cmd/dynamo-browse/main.go @@ -158,18 +158,18 @@ func main() { } keyBindingService := keybindings_service.NewService(keyBindings) - keyBindingController := controllers.NewKeyBindingController(keyBindingService, scriptController) + keyBindingController := controllers.NewKeyBindingController(keyBindingService, nil) - commandController := commandctrl.NewCommandController(inputHistoryService, - cmdpacks.StandardCommands{ - TableService: tableService, - State: state, - ReadController: tableReadController, - WriteController: tableWriteController, - ExportController: exportController, - KeyBindingController: keyBindingController, - }, + stdCommands := cmdpacks.NewStandardCommands( + tableService, + state, + tableReadController, + tableWriteController, + exportController, + keyBindingController, ) + + commandController := commandctrl.NewCommandController(inputHistoryService, stdCommands) commandController.AddCommandLookupExtension(scriptController) commandController.SetCommandCompletionProvider(columnsController) diff --git a/internal/common/ui/commandctrl/cmdpacks/modrs.go b/internal/common/ui/commandctrl/cmdpacks/modrs.go index eea66b4..407120c 100644 --- a/internal/common/ui/commandctrl/cmdpacks/modrs.go +++ b/internal/common/ui/commandctrl/cmdpacks/modrs.go @@ -3,6 +3,7 @@ package cmdpacks import ( "context" "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" + "github.com/lmika/dynamo-browse/internal/common/ui/commandctrl" "github.com/lmika/dynamo-browse/internal/dynamo-browse/controllers" "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" "github.com/lmika/dynamo-browse/internal/dynamo-browse/models/queryexpr" @@ -65,21 +66,26 @@ var rsQueryDoc = repl.Doc{ `, } -func (rs *rsModule) rsQuery(ctx context.Context, args ucl.CallArgs) (any, error) { +func parseQuery( + ctx context.Context, + args ucl.CallArgs, + currentRS *models.ResultSet, + tablesService *tables.Service, +) (*queryexpr.QueryExpr, *models.TableInfo, error) { var expr string if err := args.Bind(&expr); err != nil { - return nil, err + return nil, nil, err } q, err := queryexpr.Parse(expr) if err != nil { - return nil, err + return nil, nil, err } if args.NArgs() > 0 { var queryArgs ucl.Hashable if err := args.Bind(&queryArgs); err != nil { - return nil, err + return nil, nil, err } queryNames := map[string]string{} @@ -108,17 +114,26 @@ func (rs *rsModule) rsQuery(ctx context.Context, args ucl.CallArgs) (any, error) if args.HasSwitch("table") { var tblName string if err := args.BindSwitch("table", &tblName); err != nil { - return nil, err + return nil, nil, err } - tableInfo, err = rs.tableService.Describe(ctx, tblName) + tableInfo, err = tablesService.Describe(ctx, tblName) if err != nil { - return nil, err + return nil, nil, err } - } else if currRs := rs.state.ResultSet(); currRs != nil && currRs.TableInfo != nil { - tableInfo = currRs.TableInfo + } else if currentRS != nil && currentRS.TableInfo != nil { + tableInfo = currentRS.TableInfo } else { - return nil, errors.New("no table specified") + return nil, nil, errors.New("no table specified") + } + + return q, tableInfo, nil +} + +func (rs *rsModule) rsQuery(ctx context.Context, args ucl.CallArgs) (any, error) { + q, tableInfo, err := parseQuery(ctx, args, rs.state.ResultSet(), rs.tableService) + if err != nil { + return nil, err } newResultSet, err := rs.tableService.ScanOrQuery(context.Background(), tableInfo, q, nil) @@ -167,6 +182,20 @@ func (rs *rsModule) rsScan(ctx context.Context, args ucl.CallArgs) (_ any, err e return newResultSetProxy(newResultSet), nil } +func (rs *rsModule) rsFilter(ctx context.Context, args ucl.CallArgs) (any, error) { + var ( + rsProxy SimpleProxy[*models.ResultSet] + filter string + ) + + if err := args.Bind(&rsProxy, &filter); err != nil { + return nil, err + } + + newResultSet := rs.tableService.Filter(rsProxy.ProxyValue(), filter) + return newResultSetProxy(newResultSet), nil +} + var rsNextPageDoc = repl.Doc{ Brief: "Returns the next page of the passed in result-set", Usage: "RESULT_SET", @@ -207,6 +236,33 @@ func (rs *rsModule) rsUnion(ctx context.Context, args ucl.CallArgs) (_ any, err return newResultSetProxy(rsProxy1.ProxyValue().MergeWith(rsProxy2.ProxyValue())), nil } +func (rs *rsModule) rsSet(ctx context.Context, args ucl.CallArgs) (_ any, err error) { + var ( + item itemProxy + expr string + val ucl.Object + ) + + if err := args.Bind(&item, &expr, &val); err != nil { + return nil, err + } + + q, err := queryexpr.Parse(expr) + if err != nil { + return nil, err + } + + // TEMP + if err := q.SetEvalItem(item.item, &types.AttributeValueMemberS{Value: val.String()}); err != nil { + return nil, err + } + item.resultSet.SetDirty(item.idx, true) + commandctrl.QueueRefresh(ctx) + // END TEMP + + return item, nil +} + func moduleRS(tableService *tables.Service, state *controllers.State) ucl.Module { m := &rsModule{ tableService: tableService, @@ -219,8 +275,10 @@ func moduleRS(tableService *tables.Service, state *controllers.State) ucl.Module "new": m.rsNew, "query": m.rsQuery, "scan": m.rsScan, + "filter": m.rsFilter, "next-page": m.rsNextPage, "union": m.rsUnion, + "set": m.rsSet, }, } } diff --git a/internal/common/ui/commandctrl/cmdpacks/modui.go b/internal/common/ui/commandctrl/cmdpacks/modui.go new file mode 100644 index 0000000..6bcd6ce --- /dev/null +++ b/internal/common/ui/commandctrl/cmdpacks/modui.go @@ -0,0 +1,195 @@ +package cmdpacks + +import ( + "context" + tea "github.com/charmbracelet/bubbletea" + "github.com/lmika/dynamo-browse/internal/common/ui/commandctrl" + "github.com/lmika/dynamo-browse/internal/common/ui/events" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/controllers" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/tables" + "ucl.lmika.dev/ucl" +) + +type uiModule struct { + tableService *tables.Service + state *controllers.State + ckb *customKeyBinding + readController *controllers.TableReadController +} + +func (m *uiModule) uiPrompt(ctx context.Context, args ucl.CallArgs) (any, error) { + var prompt string + if err := args.Bind(&prompt); err != nil { + return nil, err + } + + resChan := make(chan string) + go func() { + commandctrl.PostMsg(ctx, events.PromptForInput(prompt, nil, func(value string) tea.Msg { + resChan <- value + return nil + })) + }() + + select { + case value := <-resChan: + return value, nil + case <-ctx.Done(): + return nil, ctx.Err() + } +} + +func (m *uiModule) uiConfirm(ctx context.Context, args ucl.CallArgs) (any, error) { + var prompt string + if err := args.Bind(&prompt); err != nil { + return nil, err + } + + resChan := make(chan bool) + go func() { + commandctrl.PostMsg(ctx, events.Confirm(prompt, func(value bool) tea.Msg { + resChan <- value + return nil + })) + }() + + select { + case value := <-resChan: + return value, nil + case <-ctx.Done(): + return nil, ctx.Err() + } +} + +func (m *uiModule) uiPromptTable(ctx context.Context, args ucl.CallArgs) (any, error) { + tables, err := m.tableService.ListTables(context.Background()) + if err != nil { + return nil, err + } + + resChan := make(chan string) + go func() { + commandctrl.PostMsg(ctx, controllers.PromptForTableMsg{ + Tables: tables, + OnSelected: func(tableName string) tea.Msg { + resChan <- tableName + return nil + }, + }) + }() + + select { + case value := <-resChan: + return value, nil + case <-ctx.Done(): + return nil, ctx.Err() + } +} + +func (m *uiModule) uiBind(ctx context.Context, args ucl.CallArgs) (any, error) { + var ( + bindName string + key string + inv ucl.Invokable + ) + + if args.NArgs() == 2 { + if err := args.Bind(&key, &inv); err != nil { + return nil, err + } + bindName = "custom." + key + } else { + if err := args.Bind(&bindName, &key, &inv); err != nil { + return nil, err + } + } + + invoker := commandctrl.GetInvoker(ctx) + + m.ckb.bindings[bindName] = func() tea.Msg { + return invoker.Invoke(inv, nil) + } + m.ckb.keyBindings[key] = bindName + return nil, nil +} + +func (m *uiModule) uiQuery(ctx context.Context, args ucl.CallArgs) (any, error) { + q, tableInfo, err := parseQuery(ctx, args, m.state.ResultSet(), m.tableService) + if err != nil { + return nil, err + } + + commandctrl.PostMsg(ctx, m.readController.RunQuery(q, tableInfo)) + return nil, nil +} + +func (m *uiModule) uiFilter(ctx context.Context, args ucl.CallArgs) (any, error) { + var filter string + + if err := args.Bind(&filter); err != nil { + return nil, err + } + + commandctrl.PostMsg(ctx, m.readController.Filter(filter)) + return nil, nil +} + +func moduleUI( + tableService *tables.Service, + state *controllers.State, + readController *controllers.TableReadController, +) (ucl.Module, controllers.CustomKeyBindingSource) { + m := &uiModule{ + tableService: tableService, + state: state, + readController: readController, + ckb: &customKeyBinding{ + bindings: map[string]tea.Cmd{}, + keyBindings: map[string]string{}, + }, + } + + return ucl.Module{ + Name: "ui", + Builtins: map[string]ucl.BuiltinHandler{ + "prompt": m.uiPrompt, + "prompt-table": m.uiPromptTable, + "confirm": m.uiConfirm, + "query": m.uiQuery, + "filter": m.uiFilter, + "bind": m.uiBind, + }, + }, m.ckb +} + +type customKeyBinding struct { + bindings map[string]tea.Cmd + keyBindings map[string]string +} + +func (c *customKeyBinding) LookupBinding(theKey string) string { + return c.keyBindings[theKey] +} + +func (c *customKeyBinding) CustomKeyCommand(key string) tea.Cmd { + bindingName, ok := c.keyBindings[key] + if !ok { + return nil + } + + binding, ok := c.bindings[bindingName] + if !ok { + return nil + } + + return binding +} + +func (c *customKeyBinding) UnbindKey(key string) { + delete(c.keyBindings, key) +} + +func (c *customKeyBinding) Rebind(bindingName string, newKey string) error { + c.keyBindings[newKey] = bindingName + return nil +} diff --git a/internal/common/ui/commandctrl/cmdpacks/stdcmds.go b/internal/common/ui/commandctrl/cmdpacks/stdcmds.go index e5c1aab..73d2c9c 100644 --- a/internal/common/ui/commandctrl/cmdpacks/stdcmds.go +++ b/internal/common/ui/commandctrl/cmdpacks/stdcmds.go @@ -19,6 +19,30 @@ type StandardCommands struct { WriteController *controllers.TableWriteController ExportController *controllers.ExportController KeyBindingController *controllers.KeyBindingController + + modUI ucl.Module +} + +func NewStandardCommands( + tableService *tables.Service, + state *controllers.State, + readController *controllers.TableReadController, + writeController *controllers.TableWriteController, + exportController *controllers.ExportController, + keyBindingController *controllers.KeyBindingController, +) StandardCommands { + modUI, ckbs := moduleUI(tableService, state, readController) + keyBindingController.SetCustomKeyBindingSource(ckbs) + + return StandardCommands{ + TableService: tableService, + State: state, + ReadController: readController, + WriteController: writeController, + ExportController: exportController, + KeyBindingController: keyBindingController, + modUI: modUI, + } } var cmdQuitDoc = repl.Doc{ @@ -364,6 +388,7 @@ func (sc StandardCommands) cmdRebind(ctx context.Context, args ucl.CallArgs) (an func (sc StandardCommands) InstOptions() []ucl.InstOption { return []ucl.InstOption{ ucl.WithModule(moduleRS(sc.TableService, sc.State)), + ucl.WithModule(sc.modUI), } } diff --git a/internal/common/ui/commandctrl/cmdpacks/stdcmds_test.go b/internal/common/ui/commandctrl/cmdpacks/stdcmds_test.go index 44aff3e..228c6be 100644 --- a/internal/common/ui/commandctrl/cmdpacks/stdcmds_test.go +++ b/internal/common/ui/commandctrl/cmdpacks/stdcmds_test.go @@ -14,8 +14,10 @@ import ( "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/inputhistory" "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/itemrenderer" "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/jobs" + keybindings_service "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/keybindings" "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/tables" "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/viewsnapshot" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/keybindings" "github.com/lmika/dynamo-browse/test/testdynamo" "github.com/lmika/dynamo-browse/test/testworkspace" bus "github.com/lmika/events" @@ -132,15 +134,12 @@ func newService(t *testing.T, opts ...serviceOpt) *services { columnsController := controllers.NewColumnsController(readController, eventBus) exportController := controllers.NewExportController(state, service, jobsController, columnsController, pasteboardprovider.NilProvider{}) + keyBindingService := keybindings_service.NewService(keybindings.Default()) + keyBindingController := controllers.NewKeyBindingController(keyBindingService, nil) + _ = settingsController commandController := commandctrl.NewCommandController(inputHistoryService, - cmdpacks.StandardCommands{ - State: state, - TableService: service, - ReadController: readController, - WriteController: writeController, - ExportController: exportController, - }, + cmdpacks.NewStandardCommands(service, state, readController, writeController, exportController, keyBindingController), ) s.State = state diff --git a/internal/common/ui/commandctrl/commandctrl.go b/internal/common/ui/commandctrl/commandctrl.go index 79847f0..d4de9be 100644 --- a/internal/common/ui/commandctrl/commandctrl.go +++ b/internal/common/ui/commandctrl/commandctrl.go @@ -139,8 +139,26 @@ func (c *CommandController) ExecuteAndWait(ctx context.Context, commandInput str return c.uclInst.Eval(ctx, commandInput) } +func (c *CommandController) Invoke(invokable ucl.Invokable, args []any) (msg tea.Msg) { + execCtx := execContext{ctrl: c} + ctx := context.WithValue(context.Background(), commandCtlKey, &execCtx) + + res, err := invokable.Invoke(ctx, args) + if err != nil { + msg = events.Error(err) + } else if res != nil { + msg = events.StatusMsg(fmt.Sprint(res)) + } + if execCtx.requestRefresh { + c.postMessage(events.ResultSetUpdated{}) + } + + return msg +} + func (c *CommandController) cmdLooper() { - ctx := context.WithValue(context.Background(), commandCtlKey, c) + execCtx := execContext{ctrl: c} + ctx := context.WithValue(context.Background(), commandCtlKey, &execCtx) for { select { @@ -151,6 +169,9 @@ func (c *CommandController) cmdLooper() { } else if res != nil { c.postMessage(events.StatusMsg(fmt.Sprint(res))) } + if execCtx.requestRefresh { + c.postMessage(events.ResultSetUpdated{}) + } } } } diff --git a/internal/common/ui/commandctrl/ctx.go b/internal/common/ui/commandctrl/ctx.go index 7fc70ea..fef635b 100644 --- a/internal/common/ui/commandctrl/ctx.go +++ b/internal/common/ui/commandctrl/ctx.go @@ -3,33 +3,60 @@ package commandctrl import ( "context" tea "github.com/charmbracelet/bubbletea" + "ucl.lmika.dev/ucl" ) type commandCtlKeyType struct{} var commandCtlKey = commandCtlKeyType{} +type execContext struct { + ctrl *CommandController + requestRefresh bool +} + func PostMsg(ctx context.Context, msg tea.Msg) { - cmdCtl, ok := ctx.Value(commandCtlKey).(*CommandController) + cmdCtl, ok := ctx.Value(commandCtlKey).(*execContext) if ok { - cmdCtl.postMessage(msg) + cmdCtl.ctrl.postMessage(msg) } } func SelectedItemIndex(ctx context.Context) (int, bool) { - cmdCtl, ok := ctx.Value(commandCtlKey).(*CommandController) + cmdCtl, ok := ctx.Value(commandCtlKey).(*execContext) if !ok { return 0, false } - return cmdCtl.uiStateProvider.SelectedItemIndex(), true + return cmdCtl.ctrl.uiStateProvider.SelectedItemIndex(), true } func SetSelectedItemIndex(ctx context.Context, newIdx int) tea.Msg { - cmdCtl, ok := ctx.Value(commandCtlKey).(*CommandController) + cmdCtl, ok := ctx.Value(commandCtlKey).(*execContext) if !ok { return nil } - return cmdCtl.uiStateProvider.SetSelectedItemIndex(newIdx) + return cmdCtl.ctrl.uiStateProvider.SetSelectedItemIndex(newIdx) +} + +func GetInvoker(ctx context.Context) Invoker { + cmdCtl, ok := ctx.Value(commandCtlKey).(*execContext) + if !ok { + return nil + } + + return cmdCtl.ctrl +} + +func QueueRefresh(ctx context.Context) { + cmdCtl, ok := ctx.Value(commandCtlKey).(*execContext) + if !ok { + return + } + cmdCtl.requestRefresh = true +} + +type Invoker interface { + Invoke(invokable ucl.Invokable, args []any) tea.Msg } diff --git a/internal/common/ui/events/resultset.go b/internal/common/ui/events/resultset.go new file mode 100644 index 0000000..cce739a --- /dev/null +++ b/internal/common/ui/events/resultset.go @@ -0,0 +1,5 @@ +package events + +type ResultSetUpdated struct { + StatusMessage string +} diff --git a/internal/dynamo-browse/controllers/events.go b/internal/dynamo-browse/controllers/events.go index 7af1c41..ac0e74a 100644 --- a/internal/dynamo-browse/controllers/events.go +++ b/internal/dynamo-browse/controllers/events.go @@ -81,14 +81,6 @@ type PromptForTableMsg struct { OnSelected func(tableName string) tea.Msg } -type ResultSetUpdated struct { - statusMessage string -} - -func (rs ResultSetUpdated) StatusMessage() string { - return rs.statusMessage -} - type ShowColumnOverlay struct{} type HideColumnOverlay struct{} diff --git a/internal/dynamo-browse/controllers/keybinding.go b/internal/dynamo-browse/controllers/keybinding.go index 043248f..3b9dc76 100644 --- a/internal/dynamo-browse/controllers/keybinding.go +++ b/internal/dynamo-browse/controllers/keybinding.go @@ -20,6 +20,10 @@ func NewKeyBindingController(service *keybindings.Service, customBindingSource C } } +func (kb *KeyBindingController) SetCustomKeyBindingSource(customBindingSource CustomKeyBindingSource) { + kb.customBindingSource = customBindingSource +} + func (kb *KeyBindingController) Rebind(bindingName string, newKey string, force bool) tea.Msg { existingBinding := kb.findExistingBinding(newKey) if existingBinding == "" { diff --git a/internal/dynamo-browse/controllers/tableread.go b/internal/dynamo-browse/controllers/tableread.go index e7034a5..38946f3 100644 --- a/internal/dynamo-browse/controllers/tableread.go +++ b/internal/dynamo-browse/controllers/tableread.go @@ -182,6 +182,10 @@ func (c *TableReadController) PromptForQuery() tea.Msg { } } +func (c *TableReadController) RunQuery(q *queryexpr.QueryExpr, table *models.TableInfo) tea.Msg { + return c.runQuery(table, q, "", true, nil) +} + func (c *TableReadController) runQuery( tableInfo *models.TableInfo, query *queryexpr.QueryExpr, @@ -339,27 +343,31 @@ func (c *TableReadController) Mark(op MarkOp, where string) tea.Msg { }); err != nil { return events.Error(err) } - return ResultSetUpdated{} + return events.ResultSetUpdated{} } -func (c *TableReadController) Filter() tea.Msg { +func (c *TableReadController) PromptForFilter() tea.Msg { return events.PromptForInputMsg{ Prompt: "filter: ", History: c.inputHistoryService.Iter(context.Background(), filterInputHistoryCategory), OnDone: func(value string) tea.Msg { - resultSet := c.state.ResultSet() - if resultSet == nil { - return events.StatusMsg("Result-set is nil") - } - - return NewJob(c.jobController, "Applying Filter…", func(ctx context.Context) (*models.ResultSet, error) { - newResultSet := c.tableService.Filter(resultSet, value) - return newResultSet, nil - }).OnEither(c.handleResultSetFromJobResult(value, true, false, resultSetUpdateFilter)).Submit() + return c.Filter(value) }, } } +func (c *TableReadController) Filter(value string) tea.Msg { + resultSet := c.state.ResultSet() + if resultSet == nil { + return events.StatusMsg("Result-set is nil") + } + + return NewJob(c.jobController, "Applying Filter…", func(ctx context.Context) (*models.ResultSet, error) { + newResultSet := c.tableService.Filter(resultSet, value) + return newResultSet, nil + }).OnEither(c.handleResultSetFromJobResult(value, true, false, resultSetUpdateFilter)).Submit() +} + func (c *TableReadController) handleResultSetFromJobResult( filter string, pushbackStack, errIfEmpty bool, diff --git a/internal/dynamo-browse/controllers/tablewrite.go b/internal/dynamo-browse/controllers/tablewrite.go index 033a0cb..8f248c5 100644 --- a/internal/dynamo-browse/controllers/tablewrite.go +++ b/internal/dynamo-browse/controllers/tablewrite.go @@ -44,7 +44,7 @@ func (twc *TableWriteController) ToggleMark(idx int) tea.Msg { resultSet.SetMark(idx, !resultSet.Marked(idx)) }) - return ResultSetUpdated{} + return events.ResultSetUpdated{} } func (twc *TableWriteController) NewItem() tea.Msg { @@ -148,7 +148,7 @@ func (twc *TableWriteController) setStringValue(idx int, attr *queryexpr.QueryEx }); err != nil { return events.Error(err) } - return ResultSetUpdated{} + return events.ResultSetUpdated{} }, } } @@ -181,7 +181,7 @@ func (twc *TableWriteController) setToExpressionValue(idx int, attr *queryexpr.Q }); err != nil { return events.Error(err) } - return ResultSetUpdated{} + return events.ResultSetUpdated{} }, } } @@ -205,7 +205,7 @@ func (twc *TableWriteController) setNumberValue(idx int, attr *queryexpr.QueryEx }); err != nil { return events.Error(err) } - return ResultSetUpdated{} + return events.ResultSetUpdated{} }, } } @@ -234,7 +234,7 @@ func (twc *TableWriteController) setBoolValue(idx int, attr *queryexpr.QueryExpr }); err != nil { return events.Error(err) } - return ResultSetUpdated{} + return events.ResultSetUpdated{} }, } } @@ -255,7 +255,7 @@ func (twc *TableWriteController) setNullValue(idx int, attr *queryexpr.QueryExpr }); err != nil { return events.Error(err) } - return ResultSetUpdated{} + return events.ResultSetUpdated{} } func (twc *TableWriteController) DeleteAttribute(idx int, key string) tea.Msg { @@ -291,7 +291,7 @@ func (twc *TableWriteController) DeleteAttribute(idx int, key string) tea.Msg { return events.Error(err) } - return ResultSetUpdated{} + return events.ResultSetUpdated{} } func (twc *TableWriteController) PutItems() tea.Msg { @@ -351,8 +351,8 @@ func (twc *TableWriteController) PutItems() tea.Msg { } return rs, nil }).OnDone(func(rs *models.ResultSet) tea.Msg { - return ResultSetUpdated{ - statusMessage: applyToN("", len(itemsToPut), "item", "item", " put to table"), + return events.ResultSetUpdated{ + StatusMessage: applyToN("", len(itemsToPut), "item", "item", " put to table"), } }).Submit() }, @@ -379,7 +379,7 @@ func (twc *TableWriteController) TouchItem(idx int) tea.Msg { if err := twc.tableService.PutItemAt(context.Background(), resultSet, idx); err != nil { return events.Error(err) } - return ResultSetUpdated{} + return events.ResultSetUpdated{} }, } } diff --git a/internal/dynamo-browse/ui/model.go b/internal/dynamo-browse/ui/model.go index 9acb70e..81f6926 100644 --- a/internal/dynamo-browse/ui/model.go +++ b/internal/dynamo-browse/ui/model.go @@ -276,10 +276,10 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case controllers.SetTableItemView: cmd := m.setMainViewIndex(msg.ViewIndex) return m, cmd - case controllers.ResultSetUpdated: + case events.ResultSetUpdated: return m, tea.Batch( m.tableView.Refresh(), - events.SetStatus(msg.StatusMessage()), + events.SetStatus(msg.StatusMessage), ) case tea.KeyMsg: // TODO: use modes here @@ -302,7 +302,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case key.Matches(msg, m.keyMap.PromptForQuery): return m, m.tableReadController.PromptForQuery case key.Matches(msg, m.keyMap.PromptForFilter): - return m, m.tableReadController.Filter + return m, m.tableReadController.PromptForFilter case key.Matches(msg, m.keyMap.FetchNextPage): return m, m.tableReadController.NextPage case key.Matches(msg, m.keyMap.ViewBack): From cae7509a76c4d22bda991b2c6fe489ce22cf9e33 Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Sun, 25 May 2025 13:31:00 +1000 Subject: [PATCH 11/13] Almost feature complete - Added reading of UCL scripts - Added pasteboard commands - Added ui:command which will define a proc at the top-level --- cmd/dynamo-browse/main.go | 18 +++-- go.mod | 2 +- go.sum | 2 + .../common/ui/commandctrl/cmdpacks/modpb.go | 46 ++++++++++++ .../common/ui/commandctrl/cmdpacks/modrs.go | 28 +++++++- .../common/ui/commandctrl/cmdpacks/modui.go | 15 ++++ .../common/ui/commandctrl/cmdpacks/proxy.go | 4 +- .../common/ui/commandctrl/cmdpacks/stdcmds.go | 5 ++ internal/common/ui/commandctrl/commandctrl.go | 71 +++++++++++++++---- internal/common/ui/commandctrl/ctx.go | 1 + internal/dynamo-browse/controllers/export.go | 2 +- internal/dynamo-browse/ui/model.go | 38 ++++------ 12 files changed, 185 insertions(+), 47 deletions(-) create mode 100644 internal/common/ui/commandctrl/cmdpacks/modpb.go diff --git a/cmd/dynamo-browse/main.go b/cmd/dynamo-browse/main.go index 862941d..43fec27 100644 --- a/cmd/dynamo-browse/main.go +++ b/cmd/dynamo-browse/main.go @@ -8,6 +8,7 @@ import ( "log" "net" "os" + "strings" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/dynamodb" @@ -45,6 +46,7 @@ func main() { var flagDefaultLimit = flag.Int("default-limit", 0, "default limit for queries and scans") var flagWorkspace = flag.String("w", "", "workspace file") var flagQuery = flag.String("q", "", "run query") + var flagExtDir = flag.String("ext-dir", "$HOME/.config/dynamo-browse/ext:$HOME/.config/dynamo-browse/.", "directory to search for extensions") flag.Parse() ctx := context.Background() @@ -128,7 +130,7 @@ func main() { exportController := controllers.NewExportController(state, tableService, jobsController, columnsController, pasteboardProvider) settingsController := controllers.NewSettingsController(settingStore, eventBus) keyBindings := keybindings.Default() - scriptController := controllers.NewScriptController(scriptManagerService, tableReadController, jobsController, settingsController, eventBus) + //scriptController := controllers.NewScriptController(scriptManagerService, tableReadController, jobsController, settingsController, eventBus) if *flagQuery != "" { if *flagTable == "" { @@ -167,10 +169,11 @@ func main() { tableWriteController, exportController, keyBindingController, + pasteboardProvider, ) commandController := commandctrl.NewCommandController(inputHistoryService, stdCommands) - commandController.AddCommandLookupExtension(scriptController) + //commandController.AddCommandLookupExtension(scriptController) commandController.SetCommandCompletionProvider(columnsController) model := ui.NewModel( @@ -182,7 +185,7 @@ func main() { jobsController, itemRendererService, commandController, - scriptController, + //scriptController, eventBus, keyBindingController, pasteboardProvider, @@ -196,8 +199,13 @@ func main() { p := tea.NewProgram(model, tea.WithAltScreen()) jobsController.SetMessageSender(p.Send) - scriptController.Init() - scriptController.SetMessageSender(p.Send) + //scriptController.Init() + //scriptController.SetMessageSender(p.Send) + + if err := commandController.LoadExtensions(context.Background(), strings.Split(*flagExtDir, string(os.PathListSeparator))); err != nil { + fmt.Printf("Unable to load extensions: %v", err) + } + go commandController.StartMessageSender(p.Send) log.Println("launching") diff --git a/go.mod b/go.mod index ac57b4e..3113de7 100644 --- a/go.mod +++ b/go.mod @@ -117,5 +117,5 @@ require ( golang.org/x/text v0.9.0 // indirect google.golang.org/appengine v1.6.7 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - ucl.lmika.dev v0.0.0-20250519120409-53b05b5ba6f8 // indirect + ucl.lmika.dev v0.0.0-20250525023717-3076897eb73e // indirect ) diff --git a/go.sum b/go.sum index 0aafeb1..2b9e341 100644 --- a/go.sum +++ b/go.sum @@ -460,3 +460,5 @@ ucl.lmika.dev v0.0.0-20250519114239-7ca821016e9a h1:dzBBFCY50+MQcJaQ90swdDyjzag5 ucl.lmika.dev v0.0.0-20250519114239-7ca821016e9a/go.mod h1:/MMZKm6mOMtnY4I8TYEot4Pc8dKEy+/IAQo1VdpA5EY= ucl.lmika.dev v0.0.0-20250519120409-53b05b5ba6f8 h1:h32JQi0d1MI86RaAMaEU7kvti4uSLX5XYe/nk2abApg= ucl.lmika.dev v0.0.0-20250519120409-53b05b5ba6f8/go.mod h1:/MMZKm6mOMtnY4I8TYEot4Pc8dKEy+/IAQo1VdpA5EY= +ucl.lmika.dev v0.0.0-20250525023717-3076897eb73e h1:N+HzQUunDUvdjAzbSDtHQZVZ1k+XHbVgbNwmc+EKmlQ= +ucl.lmika.dev v0.0.0-20250525023717-3076897eb73e/go.mod h1:/MMZKm6mOMtnY4I8TYEot4Pc8dKEy+/IAQo1VdpA5EY= diff --git a/internal/common/ui/commandctrl/cmdpacks/modpb.go b/internal/common/ui/commandctrl/cmdpacks/modpb.go new file mode 100644 index 0000000..528499b --- /dev/null +++ b/internal/common/ui/commandctrl/cmdpacks/modpb.go @@ -0,0 +1,46 @@ +package cmdpacks + +import ( + "context" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/providers/pasteboardprovider" + "ucl.lmika.dev/ucl" +) + +type pbModule struct { + pasteboardProvider *pasteboardprovider.Provider +} + +func (m pbModule) pbGet(ctx context.Context, args ucl.CallArgs) (any, error) { + s, ok := m.pasteboardProvider.ReadText() + if !ok { + return "", nil + } + return s, nil +} + +func (m pbModule) pbPut(ctx context.Context, args ucl.CallArgs) (any, error) { + var s string + if err := args.Bind(&s); err != nil { + return nil, err + } + if err := m.pasteboardProvider.WriteText([]byte(s)); err != nil { + return nil, err + } + return s, nil +} + +func modulePB( + pasteboardProvider *pasteboardprovider.Provider, +) ucl.Module { + m := &pbModule{ + pasteboardProvider: pasteboardProvider, + } + + return ucl.Module{ + Name: "pb", + Builtins: map[string]ucl.BuiltinHandler{ + "get": m.pbGet, + "put": m.pbPut, + }, + } +} diff --git a/internal/common/ui/commandctrl/cmdpacks/modrs.go b/internal/common/ui/commandctrl/cmdpacks/modrs.go index 407120c..148723c 100644 --- a/internal/common/ui/commandctrl/cmdpacks/modrs.go +++ b/internal/common/ui/commandctrl/cmdpacks/modrs.go @@ -252,13 +252,36 @@ func (rs *rsModule) rsSet(ctx context.Context, args ucl.CallArgs) (_ any, err er return nil, err } - // TEMP + // TEMP: attribute is always S if err := q.SetEvalItem(item.item, &types.AttributeValueMemberS{Value: val.String()}); err != nil { return nil, err } item.resultSet.SetDirty(item.idx, true) commandctrl.QueueRefresh(ctx) - // END TEMP + + return item, nil +} + +func (rs *rsModule) rsDel(ctx context.Context, args ucl.CallArgs) (_ any, err error) { + var ( + item itemProxy + expr string + ) + + if err := args.Bind(&item, &expr); err != nil { + return nil, err + } + + q, err := queryexpr.Parse(expr) + if err != nil { + return nil, err + } + + if err := q.DeleteAttribute(item.item); err != nil { + return nil, err + } + item.resultSet.SetDirty(item.idx, true) + commandctrl.QueueRefresh(ctx) return item, nil } @@ -279,6 +302,7 @@ func moduleRS(tableService *tables.Service, state *controllers.State) ucl.Module "next-page": m.rsNextPage, "union": m.rsUnion, "set": m.rsSet, + "del": m.rsDel, }, } } diff --git a/internal/common/ui/commandctrl/cmdpacks/modui.go b/internal/common/ui/commandctrl/cmdpacks/modui.go index 6bcd6ce..26253e6 100644 --- a/internal/common/ui/commandctrl/cmdpacks/modui.go +++ b/internal/common/ui/commandctrl/cmdpacks/modui.go @@ -17,6 +17,20 @@ type uiModule struct { readController *controllers.TableReadController } +func (m *uiModule) uiCommand(ctx context.Context, args ucl.CallArgs) (any, error) { + var ( + name string + cmd ucl.Invokable + ) + if err := args.Bind(&name, &cmd); err != nil { + return nil, err + } + + invoker := commandctrl.GetInvoker(ctx) + invoker.Inst().SetBuiltinInvokable(name, cmd) + return nil, nil +} + func (m *uiModule) uiPrompt(ctx context.Context, args ucl.CallArgs) (any, error) { var prompt string if err := args.Bind(&prompt); err != nil { @@ -152,6 +166,7 @@ func moduleUI( return ucl.Module{ Name: "ui", Builtins: map[string]ucl.BuiltinHandler{ + "command": m.uiCommand, "prompt": m.uiPrompt, "prompt-table": m.uiPromptTable, "confirm": m.uiConfirm, diff --git a/internal/common/ui/commandctrl/cmdpacks/proxy.go b/internal/common/ui/commandctrl/cmdpacks/proxy.go index 6801946..8dad249 100644 --- a/internal/common/ui/commandctrl/cmdpacks/proxy.go +++ b/internal/common/ui/commandctrl/cmdpacks/proxy.go @@ -126,8 +126,8 @@ var keyAttributeProxyFields = &proxyInfo[models.KeyAttribute]{ return fmt.Sprintf("KeyAttribute(%v,%v)", t.PartitionKey, t.SortKey) }, fields: map[string]func(t models.KeyAttribute) ucl.Object{ - "PartitionKey": func(t models.KeyAttribute) ucl.Object { return ucl.StringObject(t.PartitionKey) }, - "SortKey": func(t models.KeyAttribute) ucl.Object { return ucl.StringObject(t.SortKey) }, + "PK": func(t models.KeyAttribute) ucl.Object { return ucl.StringObject(t.PartitionKey) }, + "SK": func(t models.KeyAttribute) ucl.Object { return ucl.StringObject(t.SortKey) }, }, } diff --git a/internal/common/ui/commandctrl/cmdpacks/stdcmds.go b/internal/common/ui/commandctrl/cmdpacks/stdcmds.go index 73d2c9c..9019ec6 100644 --- a/internal/common/ui/commandctrl/cmdpacks/stdcmds.go +++ b/internal/common/ui/commandctrl/cmdpacks/stdcmds.go @@ -6,6 +6,7 @@ import ( "github.com/lmika/dynamo-browse/internal/common/ui/commandctrl" "github.com/lmika/dynamo-browse/internal/dynamo-browse/controllers" "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/providers/pasteboardprovider" "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/tables" "github.com/pkg/errors" "ucl.lmika.dev/repl" @@ -19,6 +20,7 @@ type StandardCommands struct { WriteController *controllers.TableWriteController ExportController *controllers.ExportController KeyBindingController *controllers.KeyBindingController + PBProvider *pasteboardprovider.Provider modUI ucl.Module } @@ -30,6 +32,7 @@ func NewStandardCommands( writeController *controllers.TableWriteController, exportController *controllers.ExportController, keyBindingController *controllers.KeyBindingController, + pbProvider *pasteboardprovider.Provider, ) StandardCommands { modUI, ckbs := moduleUI(tableService, state, readController) keyBindingController.SetCustomKeyBindingSource(ckbs) @@ -41,6 +44,7 @@ func NewStandardCommands( WriteController: writeController, ExportController: exportController, KeyBindingController: keyBindingController, + PBProvider: pbProvider, modUI: modUI, } } @@ -389,6 +393,7 @@ func (sc StandardCommands) InstOptions() []ucl.InstOption { return []ucl.InstOption{ ucl.WithModule(moduleRS(sc.TableService, sc.State)), ucl.WithModule(sc.modUI), + ucl.WithModule(modulePB(sc.PBProvider)), } } diff --git a/internal/common/ui/commandctrl/commandctrl.go b/internal/common/ui/commandctrl/commandctrl.go index d4de9be..bfbe79b 100644 --- a/internal/common/ui/commandctrl/commandctrl.go +++ b/internal/common/ui/commandctrl/commandctrl.go @@ -1,6 +1,7 @@ package commandctrl import ( + "bytes" "context" "fmt" tea "github.com/charmbracelet/bubbletea" @@ -136,7 +137,7 @@ func (c *CommandController) execute(ctx ExecContext, commandInput string) tea.Ms } func (c *CommandController) ExecuteAndWait(ctx context.Context, commandInput string) (any, error) { - return c.uclInst.Eval(ctx, commandInput) + return c.uclInst.EvalString(ctx, commandInput) } func (c *CommandController) Invoke(invokable ucl.Invokable, args []any) (msg tea.Msg) { @@ -202,7 +203,47 @@ func (c *CommandController) lookupCommand(name string) Command { return nil } -func (c *CommandController) ExecuteFile(filename string) error { +func (c *CommandController) LoadExtensions(ctx context.Context, baseDirs []string) error { + log.Printf("loading extensions: %v", baseDirs) + for _, baseDir := range baseDirs { + baseDir = os.ExpandEnv(baseDir) + descendIntoSubDirs := !strings.HasSuffix(baseDir, ".") + + if stat, err := os.Stat(baseDir); err != nil { + if os.IsNotExist(err) { + continue + } + return err + } else if !stat.IsDir() { + continue + } + + log.Printf("walking %v", baseDir) + if err := filepath.Walk(baseDir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() { + if !descendIntoSubDirs && path != baseDir { + return filepath.SkipDir + } + return nil + } + if strings.HasSuffix(info.Name(), ".ucl") { + if err := c.ExecuteFile(ctx, path); err != nil { + log.Println(err) + } + log.Printf("loaded %v\n", path) + } + return nil + }); err != nil { + return err + } + } + return nil +} + +func (c *CommandController) ExecuteFile(ctx context.Context, filename string) error { oldInteractive := c.interactive c.interactive = false defer func() { @@ -211,27 +252,31 @@ func (c *CommandController) ExecuteFile(filename string) error { baseFilename := filepath.Base(filename) + execCtx := execContext{ctrl: c} + ctx = context.WithValue(context.Background(), commandCtlKey, &execCtx) + if rcFile, err := os.ReadFile(filename); err == nil { - if err := c.executeFile(rcFile, baseFilename); err != nil { - return errors.Wrapf(err, "error executing %v", filename) + if err := c.executeFile(ctx, rcFile); err != nil { + return errors.Wrapf(err, "error executing %v", baseFilename) } } else { - return errors.Wrapf(err, "error loading %v", filename) + return errors.Wrapf(err, "error loading %v", baseFilename) } return nil } -func (c *CommandController) executeFile(file []byte, filename string) error { - //msg := c.execute(ExecContext{FromFile: true}, string(file)) - //switch m := msg.(type) { - //case events.ErrorMsg: - // log.Printf("%v: error - %v", filename, m.Error()) - //case events.StatusMsg: - // log.Printf("%v: %v", filename, string(m)) - //} +func (c *CommandController) executeFile(ctx context.Context, file []byte) error { + if _, err := c.uclInst.Eval(ctx, bytes.NewReader(file), ucl.WithSubEnv()); err != nil { + return err + } + return nil } +func (c *CommandController) Inst() *ucl.Inst { + return c.uclInst +} + func (c *CommandController) cmdInvoker(ctx context.Context, name string, args ucl.CallArgs) (any, error) { command := c.lookupCommand(name) if command == nil { diff --git a/internal/common/ui/commandctrl/ctx.go b/internal/common/ui/commandctrl/ctx.go index fef635b..dffff70 100644 --- a/internal/common/ui/commandctrl/ctx.go +++ b/internal/common/ui/commandctrl/ctx.go @@ -59,4 +59,5 @@ func QueueRefresh(ctx context.Context) { type Invoker interface { Invoke(invokable ucl.Invokable, args []any) tea.Msg + Inst() *ucl.Inst } diff --git a/internal/dynamo-browse/controllers/export.go b/internal/dynamo-browse/controllers/export.go index 7f4ce8e..23c91a9 100644 --- a/internal/dynamo-browse/controllers/export.go +++ b/internal/dynamo-browse/controllers/export.go @@ -107,7 +107,7 @@ func (c *ExportController) ExportCSVToClipboard() tea.Msg { if err := c.pasteboardProvider.WriteText(bts.Bytes()); err != nil { return events.Error(err) } - return nil + return events.StatusMsg("Table copied to clipboard") } // TODO: this really needs to be a service! diff --git a/internal/dynamo-browse/ui/model.go b/internal/dynamo-browse/ui/model.go index 81f6926..90dfa0f 100644 --- a/internal/dynamo-browse/ui/model.go +++ b/internal/dynamo-browse/ui/model.go @@ -22,7 +22,6 @@ import ( "github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/utils" bus "github.com/lmika/events" "log" - "os" ) const ( @@ -33,8 +32,6 @@ const ( ViewModeTableOnly = 4 ViewModeCount = 5 - - initRCFilename = "$HOME/.config/audax/dynamo-browse/init.rc" ) type Model struct { @@ -43,14 +40,14 @@ type Model struct { settingsController *controllers.SettingsController exportController *controllers.ExportController commandController *commandctrl.CommandController - scriptController *controllers.ScriptController - jobController *controllers.JobsController - colSelector *colselector.Model - relSelector *relselector.Model - itemEdit *dynamoitemedit.Model - statusAndPrompt *statusandprompt.StatusAndPrompt - tableSelect *tableselect.Model - eventBus *bus.Bus + //scriptController *controllers.ScriptController + jobController *controllers.JobsController + colSelector *colselector.Model + relSelector *relselector.Model + itemEdit *dynamoitemedit.Model + statusAndPrompt *statusandprompt.StatusAndPrompt + tableSelect *tableselect.Model + eventBus *bus.Bus mainViewIndex int @@ -71,7 +68,7 @@ func NewModel( jobController *controllers.JobsController, itemRendererService *itemrenderer.Service, cc *commandctrl.CommandController, - scriptController *controllers.ScriptController, + //scriptController *controllers.ScriptController, eventBus *bus.Bus, keyBindingController *controllers.KeyBindingController, pasteboardProvider services.PasteboardProvider, @@ -255,7 +252,8 @@ func NewModel( tableReadController: rc, tableWriteController: wc, commandController: cc, - scriptController: scriptController, + //scriptController: scriptController, + exportController: exportController, jobController: jobController, itemEdit: itemEdit, colSelector: colSelector, @@ -318,10 +316,10 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // return m, nil case key.Matches(msg, m.keyMap.ShowColumnOverlay): return m, events.SetTeaMessage(controllers.ShowColumnOverlay{}) - case key.Matches(msg, m.keyMap.ShowRelItemsOverlay): - if idx := m.tableView.SelectedItemIndex(); idx >= 0 { - return m, events.SetTeaMessage(m.scriptController.LookupRelatedItems(idx)) - } + //case key.Matches(msg, m.keyMap.ShowRelItemsOverlay): + // if idx := m.tableView.SelectedItemIndex(); idx >= 0 { + // return m, events.SetTeaMessage(m.scriptController.LookupRelatedItems(idx)) + // } case key.Matches(msg, m.keyMap.PromptForCommand): return m, m.commandController.Prompt case key.Matches(msg, m.keyMap.PromptForTable): @@ -344,12 +342,6 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } func (m Model) Init() tea.Cmd { - // TODO: this should probably be moved somewhere else - rcFilename := os.ExpandEnv(initRCFilename) - if err := m.commandController.ExecuteFile(rcFilename); err != nil { - log.Println(err) - } - return tea.Batch( m.tableReadController.Init, m.root.Init(), From 76dce52a86bb591ecf003992dc5055866e9d99b5 Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Mon, 26 May 2025 21:20:46 +1000 Subject: [PATCH 12/13] Added opt:set --- cmd/dynamo-browse/main.go | 6 ++- .../common/ui/commandctrl/cmdpacks/modopt.go | 47 +++++++++++++++++++ .../common/ui/commandctrl/cmdpacks/modpb.go | 6 +-- .../common/ui/commandctrl/cmdpacks/stdcmds.go | 22 +++++++-- .../ui/commandctrl/cmdpacks/stdcmds_test.go | 14 ++++-- internal/common/ui/commandctrl/commandctrl.go | 12 ++++- .../common/ui/commandctrl/commandctrl_test.go | 3 +- internal/common/ui/commandctrl/packs.go | 6 ++- .../controllers/tablewrite_test.go | 2 +- internal/dynamo-browse/ui/model.go | 16 +++---- 10 files changed, 108 insertions(+), 26 deletions(-) create mode 100644 internal/common/ui/commandctrl/cmdpacks/modopt.go diff --git a/cmd/dynamo-browse/main.go b/cmd/dynamo-browse/main.go index 43fec27..d1c4d08 100644 --- a/cmd/dynamo-browse/main.go +++ b/cmd/dynamo-browse/main.go @@ -170,9 +170,13 @@ func main() { exportController, keyBindingController, pasteboardProvider, + settingsController, ) - commandController := commandctrl.NewCommandController(inputHistoryService, stdCommands) + commandController, err := commandctrl.NewCommandController(inputHistoryService, stdCommands) + if err != nil { + cli.Fatalf("cannot setup command controller: %v", err) + } //commandController.AddCommandLookupExtension(scriptController) commandController.SetCommandCompletionProvider(columnsController) diff --git a/internal/common/ui/commandctrl/cmdpacks/modopt.go b/internal/common/ui/commandctrl/cmdpacks/modopt.go new file mode 100644 index 0000000..398a86b --- /dev/null +++ b/internal/common/ui/commandctrl/cmdpacks/modopt.go @@ -0,0 +1,47 @@ +package cmdpacks + +import ( + "context" + "github.com/lmika/dynamo-browse/internal/common/ui/commandctrl" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/controllers" + "ucl.lmika.dev/ucl" +) + +type optModule struct { + settingsController *controllers.SettingsController +} + +func (m optModule) pbSet(ctx context.Context, args ucl.CallArgs) (any, error) { + var ( + name string + newVale string + ) + + if args.NArgs() == 1 { + if err := args.Bind(&name); err != nil { + return nil, err + } + } else { + if err := args.Bind(&name, &newVale); err != nil { + return nil, err + } + } + + commandctrl.PostMsg(ctx, m.settingsController.SetSetting(name, newVale)) + return nil, nil +} + +func moduleOpt( + settingsController *controllers.SettingsController, +) ucl.Module { + m := &optModule{ + settingsController: settingsController, + } + + return ucl.Module{ + Name: "opt", + Builtins: map[string]ucl.BuiltinHandler{ + "set": m.pbSet, + }, + } +} diff --git a/internal/common/ui/commandctrl/cmdpacks/modpb.go b/internal/common/ui/commandctrl/cmdpacks/modpb.go index 528499b..2a27d6e 100644 --- a/internal/common/ui/commandctrl/cmdpacks/modpb.go +++ b/internal/common/ui/commandctrl/cmdpacks/modpb.go @@ -2,12 +2,12 @@ package cmdpacks import ( "context" - "github.com/lmika/dynamo-browse/internal/dynamo-browse/providers/pasteboardprovider" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/services" "ucl.lmika.dev/ucl" ) type pbModule struct { - pasteboardProvider *pasteboardprovider.Provider + pasteboardProvider services.PasteboardProvider } func (m pbModule) pbGet(ctx context.Context, args ucl.CallArgs) (any, error) { @@ -30,7 +30,7 @@ func (m pbModule) pbPut(ctx context.Context, args ucl.CallArgs) (any, error) { } func modulePB( - pasteboardProvider *pasteboardprovider.Provider, + pasteboardProvider services.PasteboardProvider, ) ucl.Module { m := &pbModule{ pasteboardProvider: pasteboardProvider, diff --git a/internal/common/ui/commandctrl/cmdpacks/stdcmds.go b/internal/common/ui/commandctrl/cmdpacks/stdcmds.go index 9019ec6..088c0f9 100644 --- a/internal/common/ui/commandctrl/cmdpacks/stdcmds.go +++ b/internal/common/ui/commandctrl/cmdpacks/stdcmds.go @@ -6,7 +6,7 @@ import ( "github.com/lmika/dynamo-browse/internal/common/ui/commandctrl" "github.com/lmika/dynamo-browse/internal/dynamo-browse/controllers" "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" - "github.com/lmika/dynamo-browse/internal/dynamo-browse/providers/pasteboardprovider" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/services" "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/tables" "github.com/pkg/errors" "ucl.lmika.dev/repl" @@ -20,7 +20,8 @@ type StandardCommands struct { WriteController *controllers.TableWriteController ExportController *controllers.ExportController KeyBindingController *controllers.KeyBindingController - PBProvider *pasteboardprovider.Provider + PBProvider services.PasteboardProvider + SettingsController *controllers.SettingsController modUI ucl.Module } @@ -32,7 +33,8 @@ func NewStandardCommands( writeController *controllers.TableWriteController, exportController *controllers.ExportController, keyBindingController *controllers.KeyBindingController, - pbProvider *pasteboardprovider.Provider, + pbProvider services.PasteboardProvider, + settingsController *controllers.SettingsController, ) StandardCommands { modUI, ckbs := moduleUI(tableService, state, readController) keyBindingController.SetCustomKeyBindingSource(ckbs) @@ -45,6 +47,7 @@ func NewStandardCommands( ExportController: exportController, KeyBindingController: keyBindingController, PBProvider: pbProvider, + SettingsController: settingsController, modUI: modUI, } } @@ -394,6 +397,7 @@ func (sc StandardCommands) InstOptions() []ucl.InstOption { ucl.WithModule(moduleRS(sc.TableService, sc.State)), ucl.WithModule(sc.modUI), ucl.WithModule(modulePB(sc.PBProvider)), + ucl.WithModule(moduleOpt(sc.SettingsController)), } } @@ -402,7 +406,6 @@ func (sc StandardCommands) ConfigureUCL(ucl *ucl.Inst) { ucl.SetBuiltin("table", sc.cmdTable) ucl.SetBuiltin("export", sc.cmdExport) ucl.SetBuiltin("mark", sc.cmdMark) - // unmark --> alias for { mark none } ucl.SetBuiltin("next-page", sc.cmdNextPage) ucl.SetBuiltin("delete", sc.cmdDelete) ucl.SetBuiltin("new-item", sc.cmdNewItem) @@ -413,9 +416,18 @@ func (sc StandardCommands) ConfigureUCL(ucl *ucl.Inst) { ucl.SetBuiltin("touch", sc.cmdTouch) ucl.SetBuiltin("noisy-touch", sc.cmdNoisyTouch) ucl.SetBuiltin("rebind", sc.cmdRebind) - // set-opt --> alias to opts:set ucl.SetPseudoVar("resultset", resultSetPVar{sc.State, sc.ReadController}) ucl.SetPseudoVar("table", tablePVar{sc.State}) ucl.SetPseudoVar("item", itemPVar{sc.State}) } + +func (sc StandardCommands) RunPrelude(ctx context.Context, ucl *ucl.Inst) error { + _, err := ucl.EvalString(ctx, uclPrelude) + return err +} + +const uclPrelude = ` +ui:command unmark { mark none } +ui:command set-opt { |n k| opt:set $n $k } +` diff --git a/internal/common/ui/commandctrl/cmdpacks/stdcmds_test.go b/internal/common/ui/commandctrl/cmdpacks/stdcmds_test.go index 228c6be..55bef6e 100644 --- a/internal/common/ui/commandctrl/cmdpacks/stdcmds_test.go +++ b/internal/common/ui/commandctrl/cmdpacks/stdcmds_test.go @@ -137,9 +137,17 @@ func newService(t *testing.T, opts ...serviceOpt) *services { keyBindingService := keybindings_service.NewService(keybindings.Default()) keyBindingController := controllers.NewKeyBindingController(keyBindingService, nil) - _ = settingsController - commandController := commandctrl.NewCommandController(inputHistoryService, - cmdpacks.NewStandardCommands(service, state, readController, writeController, exportController, keyBindingController), + commandController, _ := commandctrl.NewCommandController(inputHistoryService, + cmdpacks.NewStandardCommands( + service, + state, + readController, + writeController, + exportController, + keyBindingController, + pasteboardprovider.NilProvider{}, + settingsController, + ), ) s.State = state diff --git a/internal/common/ui/commandctrl/commandctrl.go b/internal/common/ui/commandctrl/commandctrl.go index bfbe79b..9075da8 100644 --- a/internal/common/ui/commandctrl/commandctrl.go +++ b/internal/common/ui/commandctrl/commandctrl.go @@ -35,7 +35,7 @@ type CommandController struct { interactive bool } -func NewCommandController(historyProvider IterProvider, pkgs ...CommandPack) *CommandController { +func NewCommandController(historyProvider IterProvider, pkgs ...CommandPack) (*CommandController, error) { cc := &CommandController{ historyProvider: historyProvider, commandList: nil, @@ -60,9 +60,17 @@ func NewCommandController(historyProvider IterProvider, pkgs ...CommandPack) *Co pkg.ConfigureUCL(cc.uclInst) } + execCtx := execContext{ctrl: cc} + ctx := context.WithValue(context.Background(), commandCtlKey, &execCtx) + for _, pkg := range pkgs { + if err := pkg.RunPrelude(ctx, cc.uclInst); err != nil { + return nil, err + } + } + go cc.cmdLooper() - return cc + return cc, nil } func (c *CommandController) AddCommands(ctx *CommandList) { diff --git a/internal/common/ui/commandctrl/commandctrl_test.go b/internal/common/ui/commandctrl/commandctrl_test.go index b21783e..bcc112a 100644 --- a/internal/common/ui/commandctrl/commandctrl_test.go +++ b/internal/common/ui/commandctrl/commandctrl_test.go @@ -12,7 +12,8 @@ import ( func TestCommandController_Prompt(t *testing.T) { t.Run("prompt user for a command", func(t *testing.T) { - cmd := commandctrl.NewCommandController(mockIterProvider{}) + cmd, err := commandctrl.NewCommandController(mockIterProvider{}) + assert.NoError(t, err) res := cmd.Prompt() diff --git a/internal/common/ui/commandctrl/packs.go b/internal/common/ui/commandctrl/packs.go index 6f5dbc6..16df613 100644 --- a/internal/common/ui/commandctrl/packs.go +++ b/internal/common/ui/commandctrl/packs.go @@ -1,8 +1,12 @@ package commandctrl -import "ucl.lmika.dev/ucl" +import ( + "context" + "ucl.lmika.dev/ucl" +) type CommandPack interface { InstOptions() []ucl.InstOption ConfigureUCL(ucl *ucl.Inst) + RunPrelude(ctx context.Context, ucl *ucl.Inst) error } diff --git a/internal/dynamo-browse/controllers/tablewrite_test.go b/internal/dynamo-browse/controllers/tablewrite_test.go index efd0c14..a407923 100644 --- a/internal/dynamo-browse/controllers/tablewrite_test.go +++ b/internal/dynamo-browse/controllers/tablewrite_test.go @@ -636,7 +636,7 @@ func newService(t *testing.T, cfg serviceConfig) *services { exportController := controllers.NewExportController(state, service, jobsController, columnsController, pasteboardprovider.NilProvider{}) scriptController := controllers.NewScriptController(scriptService, readController, jobsController, settingsController, eventBus) - commandController := commandctrl.NewCommandController(inputHistoryService) + commandController, _ := commandctrl.NewCommandController(inputHistoryService) commandController.AddCommandLookupExtension(scriptController) if cfg.isReadOnly { diff --git a/internal/dynamo-browse/ui/model.go b/internal/dynamo-browse/ui/model.go index 90dfa0f..42f7e31 100644 --- a/internal/dynamo-browse/ui/model.go +++ b/internal/dynamo-browse/ui/model.go @@ -40,14 +40,13 @@ type Model struct { settingsController *controllers.SettingsController exportController *controllers.ExportController commandController *commandctrl.CommandController - //scriptController *controllers.ScriptController - jobController *controllers.JobsController - colSelector *colselector.Model - relSelector *relselector.Model - itemEdit *dynamoitemedit.Model - statusAndPrompt *statusandprompt.StatusAndPrompt - tableSelect *tableselect.Model - eventBus *bus.Bus + jobController *controllers.JobsController + colSelector *colselector.Model + relSelector *relselector.Model + itemEdit *dynamoitemedit.Model + statusAndPrompt *statusandprompt.StatusAndPrompt + tableSelect *tableselect.Model + eventBus *bus.Bus mainViewIndex int @@ -68,7 +67,6 @@ func NewModel( jobController *controllers.JobsController, itemRendererService *itemrenderer.Service, cc *commandctrl.CommandController, - //scriptController *controllers.ScriptController, eventBus *bus.Bus, keyBindingController *controllers.KeyBindingController, pasteboardProvider services.PasteboardProvider, From 291d1439f4bdbb66bd8de655efa68177d0ccc0e2 Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Mon, 26 May 2025 21:45:36 +1000 Subject: [PATCH 13/13] Removed the script controller --- cmd/dynamo-browse/main.go | 8 - go.mod | 49 +- go.sum | 239 +--------- .../common/ui/commandctrl/cmdpacks/stdcmds.go | 7 + .../ui/commandctrl/cmdpacks/stdcmds_test.go | 1 - internal/dynamo-browse/controllers/scripts.go | 290 ------------ .../dynamo-browse/controllers/scripts_test.go | 192 -------- .../dynamo-browse/controllers/tableread.go | 3 - .../controllers/tablewrite_test.go | 12 - .../services/scriptmanager/builtins.go | 102 ----- .../services/scriptmanager/iface.go | 38 -- .../scriptmanager/mocks/SessionService.go | 216 --------- .../services/scriptmanager/mocks/UIService.go | 116 ----- .../services/scriptmanager/modext.go | 270 ----------- .../services/scriptmanager/modext_test.go | 151 ------- .../services/scriptmanager/modos_test.go | 56 --- .../services/scriptmanager/modsession.go | 145 ------ .../services/scriptmanager/modsession_test.go | 426 ------------------ .../services/scriptmanager/modui.go | 58 --- .../services/scriptmanager/modui_test.go | 100 ---- .../services/scriptmanager/opts.go | 26 -- .../services/scriptmanager/relitem.go | 57 --- .../services/scriptmanager/resultsetproxy.go | 337 -------------- .../scriptmanager/resultsetproxy_test.go | 355 --------------- .../services/scriptmanager/scrsched.go | 53 --- .../services/scriptmanager/service.go | 250 ---------- .../services/scriptmanager/service_test.go | 150 ------ .../services/scriptmanager/serviceopts.go | 11 - .../services/scriptmanager/tableproxy.go | 138 ------ .../services/scriptmanager/typemapping.go | 135 ------ .../services/scriptmanager/types.go | 35 -- internal/dynamo-browse/ui/model.go | 160 ------- 32 files changed, 24 insertions(+), 4162 deletions(-) delete mode 100644 internal/dynamo-browse/controllers/scripts.go delete mode 100644 internal/dynamo-browse/controllers/scripts_test.go delete mode 100644 internal/dynamo-browse/services/scriptmanager/builtins.go delete mode 100644 internal/dynamo-browse/services/scriptmanager/iface.go delete mode 100644 internal/dynamo-browse/services/scriptmanager/mocks/SessionService.go delete mode 100644 internal/dynamo-browse/services/scriptmanager/mocks/UIService.go delete mode 100644 internal/dynamo-browse/services/scriptmanager/modext.go delete mode 100644 internal/dynamo-browse/services/scriptmanager/modext_test.go delete mode 100644 internal/dynamo-browse/services/scriptmanager/modos_test.go delete mode 100644 internal/dynamo-browse/services/scriptmanager/modsession.go delete mode 100644 internal/dynamo-browse/services/scriptmanager/modsession_test.go delete mode 100644 internal/dynamo-browse/services/scriptmanager/modui.go delete mode 100644 internal/dynamo-browse/services/scriptmanager/modui_test.go delete mode 100644 internal/dynamo-browse/services/scriptmanager/opts.go delete mode 100644 internal/dynamo-browse/services/scriptmanager/relitem.go delete mode 100644 internal/dynamo-browse/services/scriptmanager/resultsetproxy.go delete mode 100644 internal/dynamo-browse/services/scriptmanager/resultsetproxy_test.go delete mode 100644 internal/dynamo-browse/services/scriptmanager/scrsched.go delete mode 100644 internal/dynamo-browse/services/scriptmanager/service.go delete mode 100644 internal/dynamo-browse/services/scriptmanager/service_test.go delete mode 100644 internal/dynamo-browse/services/scriptmanager/serviceopts.go delete mode 100644 internal/dynamo-browse/services/scriptmanager/tableproxy.go delete mode 100644 internal/dynamo-browse/services/scriptmanager/typemapping.go delete mode 100644 internal/dynamo-browse/services/scriptmanager/types.go diff --git a/cmd/dynamo-browse/main.go b/cmd/dynamo-browse/main.go index d1c4d08..3c5c4bf 100644 --- a/cmd/dynamo-browse/main.go +++ b/cmd/dynamo-browse/main.go @@ -28,7 +28,6 @@ import ( "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/itemrenderer" "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/jobs" keybindings_service "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/keybindings" - "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/scriptmanager" "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/tables" "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/viewsnapshot" "github.com/lmika/dynamo-browse/internal/dynamo-browse/ui" @@ -107,7 +106,6 @@ func main() { tableService := tables.NewService(dynamoProvider, settingStore) workspaceService := viewsnapshot.NewService(resultSetSnapshotStore) itemRendererService := itemrenderer.NewService(uiStyles.ItemView.FieldType, uiStyles.ItemView.MetaInfo) - scriptManagerService := scriptmanager.New() jobsService := jobs.NewService(eventBus) inputHistoryService := inputhistory.New(inputHistoryStore) @@ -122,7 +120,6 @@ func main() { inputHistoryService, eventBus, pasteboardProvider, - scriptManagerService, *flagTable, ) tableWriteController := controllers.NewTableWriteController(state, tableService, jobsController, tableReadController, settingStore) @@ -130,7 +127,6 @@ func main() { exportController := controllers.NewExportController(state, tableService, jobsController, columnsController, pasteboardProvider) settingsController := controllers.NewSettingsController(settingStore, eventBus) keyBindings := keybindings.Default() - //scriptController := controllers.NewScriptController(scriptManagerService, tableReadController, jobsController, settingsController, eventBus) if *flagQuery != "" { if *flagTable == "" { @@ -177,7 +173,6 @@ func main() { if err != nil { cli.Fatalf("cannot setup command controller: %v", err) } - //commandController.AddCommandLookupExtension(scriptController) commandController.SetCommandCompletionProvider(columnsController) model := ui.NewModel( @@ -189,7 +184,6 @@ func main() { jobsController, itemRendererService, commandController, - //scriptController, eventBus, keyBindingController, pasteboardProvider, @@ -203,8 +197,6 @@ func main() { p := tea.NewProgram(model, tea.WithAltScreen()) jobsController.SetMessageSender(p.Send) - //scriptController.Init() - //scriptController.SetMessageSender(p.Send) if err := commandController.LoadExtensions(context.Background(), strings.Split(*flagExtDir, string(os.PathListSeparator))); err != nil { fmt.Printf("Unable to load extensions: %v", err) diff --git a/go.mod b/go.mod index 3113de7..18738aa 100644 --- a/go.mod +++ b/go.mod @@ -13,14 +13,11 @@ require ( github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.10.12 github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression v1.4.39 github.com/aws/aws-sdk-go-v2/service/dynamodb v1.19.11 - github.com/aws/aws-sdk-go-v2/service/sqs v1.23.2 - github.com/aws/aws-sdk-go-v2/service/ssm v1.24.0 github.com/brianvoe/gofakeit/v6 v6.15.0 github.com/calyptia/go-bubble-table v0.2.1 github.com/charmbracelet/bubbles v0.14.0 github.com/charmbracelet/bubbletea v0.22.1 github.com/charmbracelet/lipgloss v0.6.0 - github.com/cloudcmds/tamarin v1.0.0 github.com/lmika/events v0.0.0-20200906102219-a2269cd4394e github.com/lmika/go-bubble-table v0.2.2-0.20220616114432-6bbb2995e538 github.com/lmika/gopkgs v0.0.0-20240408110817-a02f6fc67d1f @@ -32,65 +29,33 @@ require ( github.com/stretchr/testify v1.9.0 golang.design/x/clipboard v0.6.2 golang.org/x/exp v0.0.0-20230108222341-4b8118a2686a + ucl.lmika.dev v0.0.0-20250525023717-3076897eb73e ) require ( github.com/DataDog/zstd v1.5.2 // indirect github.com/Sereal/Sereal v0.0.0-20220220040404-e0d1e550e879 // indirect - github.com/anthonynsimon/bild v0.13.0 // indirect github.com/atotto/clipboard v0.1.4 // indirect - github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.4 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.34 // indirect github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.28 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.3.35 // indirect - github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.26 // indirect - github.com/aws/aws-sdk-go-v2/service/cloudformation v1.30.0 // indirect - github.com/aws/aws-sdk-go-v2/service/cloudfront v1.26.8 // indirect - github.com/aws/aws-sdk-go-v2/service/cloudtrail v1.27.1 // indirect - github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.26.2 // indirect github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.14.3 // indirect - github.com/aws/aws-sdk-go-v2/service/ebs v1.16.14 // indirect - github.com/aws/aws-sdk-go-v2/service/ec2 v1.102.0 // indirect - github.com/aws/aws-sdk-go-v2/service/ecr v1.18.13 // indirect - github.com/aws/aws-sdk-go-v2/service/ecs v1.27.4 // indirect - github.com/aws/aws-sdk-go-v2/service/eks v1.27.14 // indirect - github.com/aws/aws-sdk-go-v2/service/elasticache v1.27.2 // indirect - github.com/aws/aws-sdk-go-v2/service/elasticsearchservice v1.19.2 // indirect - github.com/aws/aws-sdk-go-v2/service/glue v1.52.0 // indirect - github.com/aws/aws-sdk-go-v2/service/iam v1.21.0 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.29 // indirect github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.7.28 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.28 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.3 // indirect - github.com/aws/aws-sdk-go-v2/service/kinesis v1.17.14 // indirect - github.com/aws/aws-sdk-go-v2/service/kms v1.22.2 // indirect - github.com/aws/aws-sdk-go-v2/service/lambda v1.37.0 // indirect - github.com/aws/aws-sdk-go-v2/service/rds v1.46.0 // indirect - github.com/aws/aws-sdk-go-v2/service/redshift v1.28.0 // indirect - github.com/aws/aws-sdk-go-v2/service/route53 v1.28.3 // indirect - github.com/aws/aws-sdk-go-v2/service/s3 v1.36.0 // indirect - github.com/aws/aws-sdk-go-v2/service/sfn v1.18.0 // indirect - github.com/aws/aws-sdk-go-v2/service/sns v1.20.13 // indirect github.com/aws/aws-sdk-go-v2/service/sso v1.12.12 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.12 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.19.2 // indirect - github.com/aws/aws-sdk-go-v2/service/wafv2 v1.35.1 // indirect github.com/aws/smithy-go v1.13.5 // indirect github.com/aymanbagabas/go-osc52 v1.0.3 // indirect github.com/containerd/console v1.0.3 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/gofrs/uuid v4.4.0+incompatible // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/golang/snappy v0.0.4 // indirect - github.com/hashicorp/errwrap v1.0.0 // indirect - github.com/hashicorp/go-multierror v1.1.1 // indirect - github.com/jackc/pgpassfile v1.0.0 // indirect - github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect - github.com/jackc/pgx/v5 v5.4.1 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/juju/ansiterm v0.0.0-20210929141451-8b71cc96ebdc // indirect + github.com/kr/pretty v0.3.1 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/lunixbochs/vtclean v1.0.0 // indirect github.com/mattn/go-isatty v0.0.17 // indirect @@ -98,24 +63,18 @@ require ( github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/termenv v0.13.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/risor-io/risor v1.4.0 // indirect github.com/rivo/uniseg v0.4.2 // indirect + github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/sahilm/fuzzy v0.1.0 // indirect - github.com/stretchr/objx v0.5.2 // indirect - github.com/tidwall/gjson v1.14.3 // indirect - github.com/tidwall/match v1.1.1 // indirect - github.com/tidwall/pretty v1.2.1 // indirect github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect - github.com/wI2L/jsondiff v0.3.0 // indirect go.etcd.io/bbolt v1.3.6 // indirect - golang.org/x/crypto v0.9.0 // indirect golang.org/x/exp/shiny v0.0.0-20230213192124-5e25df0256eb // indirect golang.org/x/image v0.5.0 // indirect golang.org/x/mobile v0.0.0-20210716004757-34ab1303b554 // indirect + golang.org/x/net v0.10.0 // indirect golang.org/x/sys v0.8.0 // indirect golang.org/x/term v0.8.0 // indirect golang.org/x/text v0.9.0 // indirect google.golang.org/appengine v1.6.7 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - ucl.lmika.dev v0.0.0-20250525023717-3076897eb73e // indirect ) diff --git a/go.sum b/go.sum index 2b9e341..462dd4c 100644 --- a/go.sum +++ b/go.sum @@ -1,150 +1,57 @@ -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/DataDog/zstd v1.5.2 h1:vUG4lAyuPCXO0TLbXvPv7EB7cNK1QV/luu55UHLrrn8= github.com/DataDog/zstd v1.5.2/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= github.com/Sereal/Sereal v0.0.0-20220220040404-e0d1e550e879 h1:M5ptEKnqKqpFTKbe+p5zEf3ro1deJ6opUz5j3g3/ErQ= github.com/Sereal/Sereal v0.0.0-20220220040404-e0d1e550e879/go.mod h1:D0JMgToj/WdxCgd30Kc1UcA9E+WdZoJqeVOuYW7iTBM= -github.com/alecthomas/assert/v2 v2.0.3 h1:WKqJODfOiQG0nEJKFKzDIG3E29CN2/4zR9XGJzKIkbg= -github.com/alecthomas/participle v0.7.1 h1:2bN7reTw//5f0cugJcTOnY/NYZcWQOaajW+BwZB5xWs= -github.com/alecthomas/participle v0.7.1/go.mod h1:HfdmEuwvr12HXQN44HPWXR0lHmVolVYe4dyL6lQ3duY= -github.com/alecthomas/participle/v2 v2.0.0-beta.5 h1:y6dsSYVb1G5eK6mgmy+BgI3Mw35a3WghArZ/Hbebrjo= -github.com/alecthomas/participle/v2 v2.0.0-beta.5/go.mod h1:RC764t6n4L8D8ITAJv0qdokritYSNR3wV5cVwmIEaMM= +github.com/alecthomas/assert/v2 v2.3.0 h1:mAsH2wmvjsuvyBvAmCtm7zFsBlb8mIHx5ySLVdDZXL0= +github.com/alecthomas/assert/v2 v2.3.0/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ= github.com/alecthomas/participle/v2 v2.1.1 h1:hrjKESvSqGHzRb4yW1ciisFJ4p3MGYih6icjJvbsmV8= github.com/alecthomas/participle/v2 v2.1.1/go.mod h1:Y1+hAs8DHPmc3YUFzqllV+eSQ9ljPTk0ZkPMtEdAx2c= -github.com/alecthomas/repr v0.1.0 h1:ENn2e1+J3k09gyj2shc0dHr/yjaWSHRlrJ4DPMevDqE= -github.com/anthonynsimon/bild v0.13.0 h1:mN3tMaNds1wBWi1BrJq0ipDBhpkooYfu7ZFSMhXt1C8= -github.com/anthonynsimon/bild v0.13.0/go.mod h1:tpzzp0aYkAsMi1zmfhimaDyX1xjn2OUc1AJZK/TF0AE= -github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk= +github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/asdine/storm v2.1.2+incompatible h1:dczuIkyqwY2LrtXPz8ixMrU/OFgZp71kbKTHGrXYt/Q= github.com/asdine/storm v2.1.2+incompatible/go.mod h1:RarYDc9hq1UPLImuiXK3BIWPJLdIygvV3PsInK0FbVQ= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= -github.com/aws/aws-sdk-go-v2 v1.13.0/go.mod h1:L6+ZpqHaLbAaxsqV0L4cvxZY7QupWJB4fhkf8LXvC7w= -github.com/aws/aws-sdk-go-v2 v1.16.1/go.mod h1:ytwTPBG6fXTZLxxeeCCWj2/EMYp/xDUgX+OET6TLNNU= -github.com/aws/aws-sdk-go-v2 v1.17.4 h1:wyC6p9Yfq6V2y98wfDsj6OnNQa4w2BLGCLIxzNhwOGY= github.com/aws/aws-sdk-go-v2 v1.17.4/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw= github.com/aws/aws-sdk-go-v2 v1.18.1 h1:+tefE750oAb7ZQGzla6bLkOwfcQCEtC5y2RqoqCeqKo= github.com/aws/aws-sdk-go-v2 v1.18.1/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 h1:dK82zF6kkPeCo8J1e+tGx4JdvDIQzj7ygIoLg8WMuGs= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10/go.mod h1:VeTZetY5KRJLuD/7fkQXMU6Mw7H5m/KP2J5Iy9osMno= -github.com/aws/aws-sdk-go-v2/config v1.13.1 h1:yLv8bfNoT4r+UvUKQKqRtdnvuWGMK5a82l4ru9Jvnuo= -github.com/aws/aws-sdk-go-v2/config v1.13.1/go.mod h1:Ba5Z4yL/UGbjQUzsiaN378YobhFo0MLfueXGiOsYtEs= github.com/aws/aws-sdk-go-v2/config v1.18.27 h1:Az9uLwmssTE6OGTpsFqOnaGpLnKDqNYOJzWuC6UAYzA= github.com/aws/aws-sdk-go-v2/config v1.18.27/go.mod h1:0My+YgmkGxeqjXZb5BYme5pc4drjTnM+x1GJ3zv42Nw= -github.com/aws/aws-sdk-go-v2/credentials v1.8.0 h1:8Ow0WcyDesGNL0No11jcgb1JAtE+WtubqXjgxau+S0o= -github.com/aws/aws-sdk-go-v2/credentials v1.8.0/go.mod h1:gnMo58Vwx3Mu7hj1wpcG8DI0s57c9o42UQ6wgTQT5to= github.com/aws/aws-sdk-go-v2/credentials v1.13.26 h1:qmU+yhKmOCyujmuPY7tf5MxR/RKyZrOPO3V4DobiTUk= github.com/aws/aws-sdk-go-v2/credentials v1.13.26/go.mod h1:GoXt2YC8jHUBbA4jr+W3JiemnIbkXOfxSXcisUsZ3os= github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.10.12 h1:ama2cD4WaH6+8Gq/M/g+ZumPmmqCyanr+6Sm+iJVxfA= github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.10.12/go.mod h1:tPnUO5mS3JThpwfq4Q8iPd745s7yh6fGPqDUEBw+Wv4= github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression v1.4.39 h1:PhgfvgqwMFQKwOcxLV7V3lNDVnR3ZUWzoB6T9oCFpR4= github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression v1.4.39/go.mod h1:/GkvC7uHpK50ilKkKx9I2gZiI/ieZbKjS2aah1rT9uE= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.10.0 h1:NITDuUZO34mqtOwFWZiXo7yAHj7kf+XPE+EiKuCBNUI= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.10.0/go.mod h1:I6/fHT/fH460v09eg2gVrd8B/IqskhNdpcLH0WNO3QI= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.4 h1:LxK/bitrAr4lnh9LnIS6i7zWbCOdMsfzKFBI6LUCS0I= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.4/go.mod h1:E1hLXN/BL2e6YizK1zFlYd8vsfi2GTjbjBazinMmeaM= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.4/go.mod h1:XHgQ7Hz2WY2GAn//UXHofLfPXWh+s62MbMOijrg12Lw= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.8/go.mod h1:LnTQMTqbKsbtt+UI5+wPsB7jedW+2ZgozoPG8k6cMxg= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.28 h1:r+XwaCLpIvCKjBIYy/HVZujQS9tsz5ohHG3ZIe0wKoE= github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.28/go.mod h1:3lwChorpIM/BhImY/hy+Z6jekmN92cXGPI1QJasVPYY= github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.34 h1:A5UqQEmPaCFpedKouS4v+dHCTUo2sKqhoKO9U5kxyWo= github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.34/go.mod h1:wZpTEecJe0Btj3IYnDx/VlUzor9wm3fJHyvLpQF0VwY= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.2.0/go.mod h1:BsCSJHx5DnDXIrOcqB8KN1/B+hXLG/bi4Y6Vjcx/x9E= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.2/go.mod h1:1x4ZP3Z8odssdhuLI+/1Tqw6Pt/VAaP4Tr8EUxHvPXE= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.22 h1:7AwGYXDdqRQYsluvKFmWoqpcOQJ4bH634SkYf3FNj/A= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.22/go.mod h1:EqK7gVrIGAHyZItrD1D8B0ilgwMD1GiWAmbU4u/JHNk= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.28 h1:srIVS45eQuewqz6fKKu6ZGXaq6FuFg5NzgQBAM6g8Y4= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.28/go.mod h1:7VRpKQQedkfIEXb4k52I7swUnZP0wohVajJMRn3vsUw= -github.com/aws/aws-sdk-go-v2/internal/ini v1.3.5 h1:ixotxbfTCFpqbuwFv/RcZwyzhkxPSYDYEMcj4niB5Uk= -github.com/aws/aws-sdk-go-v2/internal/ini v1.3.5/go.mod h1:R3sWUqPcfXSiF/LSFJhjyJmpg9uV6yP2yv3YZZjldVI= github.com/aws/aws-sdk-go-v2/internal/ini v1.3.35 h1:LWA+3kDM8ly001vJ1X1waCuLJdtTl48gwkPKWy9sosI= github.com/aws/aws-sdk-go-v2/internal/ini v1.3.35/go.mod h1:0Eg1YjxE0Bhn56lx+SHJwCzhW+2JGtizsrx+lCqrfm0= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.26 h1:wscW+pnn3J1OYnanMnza5ZVYXLX4cKk5rAvUAl4Qu+c= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.26/go.mod h1:MtYiox5gvyB+OyP0Mr0Sm/yzbEAIPL9eijj/ouHAPw0= -github.com/aws/aws-sdk-go-v2/service/cloudformation v1.30.0 h1:XbDkc4FLeg1RfnqeblfbJvaEabqq9ByZl4zqyPFkfSc= -github.com/aws/aws-sdk-go-v2/service/cloudformation v1.30.0/go.mod h1:SwQFcCs9Rog8hSHm+81KBkAK+UKLXErA/1ChaEI8mLE= -github.com/aws/aws-sdk-go-v2/service/cloudfront v1.26.8 h1:loRDtQ0vT0+JCB0hQBCfv95tttEzJ1rqSaTDy5cpy0A= -github.com/aws/aws-sdk-go-v2/service/cloudfront v1.26.8/go.mod h1:YTd4wGn2beCF9wkSTpEcupk79zDFYJk2Ca76B8YyvJg= -github.com/aws/aws-sdk-go-v2/service/cloudtrail v1.27.1 h1:Qw1G/M7eanpm6s/URkG1UuRLKEnRnpUvkUb7NMVvWb8= -github.com/aws/aws-sdk-go-v2/service/cloudtrail v1.27.1/go.mod h1:oKRYqorIUkfAVmX03+lpv3tW5WelDpaliqzTwmCj/k8= -github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.26.2 h1:PWGu2JhCb/XJlJ7SSFJq76pxk4xWsN76nZxh7TzMHx0= -github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.26.2/go.mod h1:2KOZkkzMDZCo/aLzPhys06mHNkiU74u85aMJA3PLRvg= -github.com/aws/aws-sdk-go-v2/service/dynamodb v1.18.3 h1:MxOpCZ+o9+AIeQHi2ocW7H4D7p0LhEkmetETVvDnkvg= github.com/aws/aws-sdk-go-v2/service/dynamodb v1.18.3/go.mod h1:nkpC9xkh+3vdxmhqN8Ac10pgV14DsJDLzUsV2CcS+44= github.com/aws/aws-sdk-go-v2/service/dynamodb v1.19.11 h1:tLTGNAsazbfjfjW1k/i43kyCcyTTTTFaD93H7JbSbbs= github.com/aws/aws-sdk-go-v2/service/dynamodb v1.19.11/go.mod h1:W1oiFegjVosgjIwb2Vv45jiCQT1ee8x85u8EyZRYLes= github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.14.3 h1:B+bkmCnNJi194pu9aTtYUe8f4EPXafC+xfU+zciVxdg= github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.14.3/go.mod h1:bRphLmXQD9Ux4jLcFEwyrWdmuPTj2Lh8VGl9wILuJII= -github.com/aws/aws-sdk-go-v2/service/ebs v1.16.14 h1:DosI4CvEUo6/V21pDspzYkOa2X3Zwy5XS/cbPFiqDv0= -github.com/aws/aws-sdk-go-v2/service/ebs v1.16.14/go.mod h1:yVTqVHjnrbAj6FvhTQfjNgwQbjPbDUUvA1x4IpXFmrE= -github.com/aws/aws-sdk-go-v2/service/ec2 v1.102.0 h1:P4dyjm49F2kKws0FpouBC6fjVImACXKt752+CWa01lM= -github.com/aws/aws-sdk-go-v2/service/ec2 v1.102.0/go.mod h1:tIctCeX9IbzsUTKHt53SVEcgyfxV2ElxJeEB+QUbc4M= -github.com/aws/aws-sdk-go-v2/service/ecr v1.18.13 h1:hF7MUVNjubetjggZDtn3AmqCJzD7EUi//tSdxMYPm7U= -github.com/aws/aws-sdk-go-v2/service/ecr v1.18.13/go.mod h1:XwEFO35g0uN/SftK0asWxh8Rk6DOx37R83TmWe2tzEE= -github.com/aws/aws-sdk-go-v2/service/ecs v1.27.4 h1:F1N0Eh5EGRRY9QpF+tMTkx8Wb59DkQWE91Xza/9dk1c= -github.com/aws/aws-sdk-go-v2/service/ecs v1.27.4/go.mod h1:0irnFofeEZwT7uTjSkNVcSQJbWRqZ9BRoxhKjt1BObM= -github.com/aws/aws-sdk-go-v2/service/eks v1.27.14 h1:47HQVuJXgwvuoc4AT3rVdm77H0qGFbFnsuE4PRT+xX0= -github.com/aws/aws-sdk-go-v2/service/eks v1.27.14/go.mod h1:QxuWcm9rlLkW3aEV8tiDzqZewnNSNUZfnqJvo1Nv9A0= -github.com/aws/aws-sdk-go-v2/service/elasticache v1.27.2 h1:IC9XLGcT3yEkziTlX7PX54km7cHJnltlV7Ppwq2+7ik= -github.com/aws/aws-sdk-go-v2/service/elasticache v1.27.2/go.mod h1:+oJhn/SIud310/2LLSVmlNZmExmlYPaGCLmUsnq5JZc= -github.com/aws/aws-sdk-go-v2/service/elasticsearchservice v1.19.2 h1:Zam6yofBgdtP13laNoeA+DA9wlKJNooU8p3CWw6xLaI= -github.com/aws/aws-sdk-go-v2/service/elasticsearchservice v1.19.2/go.mod h1:dehjpZ00q0RJcBUOUEysaj7zHK2rHSS4ePp89MsFiaI= -github.com/aws/aws-sdk-go-v2/service/glue v1.52.0 h1:ukSf8ZdoZ6AygsUWIjj177wLOXljxBspBaNMgvx6fRA= -github.com/aws/aws-sdk-go-v2/service/glue v1.52.0/go.mod h1:wMCE0B6l8eHb57l2DMYCGxt0rHIfcu3RvIY7SAfc+Fs= -github.com/aws/aws-sdk-go-v2/service/iam v1.21.0 h1:8hEpu60CWlrp7iEBUFRZhgPoX6+gadaGL1sD4LoRYS0= -github.com/aws/aws-sdk-go-v2/service/iam v1.21.0/go.mod h1:aQZ8BI+reeaY7RI/QQp7TKCSUHOesTdrzzylp3CW85c= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11 h1:y2+VQzC6Zh2ojtV2LoC0MNwHWc6qXv/j2vrQtlftkdA= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11/go.mod h1:iV4q2hsqtNECrfmlXyord9u4zyuFEJX9eLgLpSPzWA8= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.29 h1:zZSLP3v3riMOP14H7b4XP0uyfREDQOYv2cqIrvTXDNQ= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.29/go.mod h1:z7EjRjVwZ6pWcWdI2H64dKttvzaP99jRIj5hphW0M5U= -github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.7.22 h1:6zEryIiJOSk5/OcVHzkPDwzNBQ2atYCTShyA7TqkuxA= github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.7.22/go.mod h1:moeOz5SKfY0p6pNIChdPIQdfaUfWI67+OVe0/r6+aGY= github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.7.28 h1:/D994rtMQd1jQ2OY+7tvUlMlrv1L1c7Xtma/FhkbVtY= github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.7.28/go.mod h1:3bJI2pLY3ilrqO5EclusI1GbjFJh1iXYrhOItf2sjKw= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.7.0 h1:4QAOB3KrvI1ApJK14sliGr3Ie2pjyvNypn/lfzDHfUw= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.7.0/go.mod h1:K/qPe6AP2TGYv4l6n7c88zh9jWBDf6nHhvg1fx/EWfU= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.28 h1:bkRyG4a929RCnpVSTvLM2j/T4ls015ZhhYApbmYs15s= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.28/go.mod h1:jj7znCIg05jXlaGBlFMGP8+7UN3VtCkRBG2spnmRQkU= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.3 h1:dBL3StFxHtpBzJJ/mNEsjXVgfO+7jR0dAIEwLqMapEA= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.3/go.mod h1:f1QyiAsvIv4B49DmCqrhlXqyaR+0IxMmyX+1P+AnzOM= -github.com/aws/aws-sdk-go-v2/service/kinesis v1.17.14 h1:oSw0SQN9cKeYvCUYfPul7bH11b8E9I9BnoVUme3iSaU= -github.com/aws/aws-sdk-go-v2/service/kinesis v1.17.14/go.mod h1:omXkSCk1T1difhE8wVaecXNeerY6jmpFFu49ngjEDQk= -github.com/aws/aws-sdk-go-v2/service/kms v1.22.2 h1:jwmtdM1/l1DRNy5jQrrYpsQm8zwetkgeqhAqefDr1yI= -github.com/aws/aws-sdk-go-v2/service/kms v1.22.2/go.mod h1:aNfh11Smy55o65PB3MyKbkM8BFyFUcZmj1k+4g8eNfg= -github.com/aws/aws-sdk-go-v2/service/lambda v1.37.0 h1:xzyM5ZR9kZW0/Bkw5EiihOy6B+BYclp5K+yb6OHjc7s= -github.com/aws/aws-sdk-go-v2/service/lambda v1.37.0/go.mod h1:Q8zQi5nZpjUF/H55dKEpKfEvFWJkgZzjjqvDb2AR5b4= -github.com/aws/aws-sdk-go-v2/service/rds v1.46.0 h1:uv2LAciZRd5lEXzJo2u92tdZh/JxcVL7YLC51D4NLG4= -github.com/aws/aws-sdk-go-v2/service/rds v1.46.0/go.mod h1:goBDR4OPrsnKpYyU0GHGcEnlTmL8O+eKGsWeyOAFJ5M= -github.com/aws/aws-sdk-go-v2/service/redshift v1.28.0 h1:tmhg03t7nNVSFqhxb8YpHqq8H1wwwrfEQv/rL7NkTAE= -github.com/aws/aws-sdk-go-v2/service/redshift v1.28.0/go.mod h1:x9am33DT5lVKUb0DH1UVbX+iFfpIqAKx6DAqB5Qu6jU= -github.com/aws/aws-sdk-go-v2/service/route53 v1.28.3 h1:nJbE4+tHd8xpM1RB1ZF0/xTJnFd/ATz42ZC35lwXx0w= -github.com/aws/aws-sdk-go-v2/service/route53 v1.28.3/go.mod h1:Cd4MnFoV+6fELBrgWXJ4Y09FrSkn/VjNPkOr1Yr1muU= -github.com/aws/aws-sdk-go-v2/service/s3 v1.36.0 h1:lEmQ1XSD9qLk+NZXbgvLJI/IiTz7OIR2TYUTFH25EI4= -github.com/aws/aws-sdk-go-v2/service/s3 v1.36.0/go.mod h1:aVbf0sko/TsLWHx30c/uVu7c62+0EAJ3vbxaJga0xCw= -github.com/aws/aws-sdk-go-v2/service/sfn v1.18.0 h1:1AIwJvCywFO4nGtHj7ZtKb9mhLpB5hToyjtE5OO6o/I= -github.com/aws/aws-sdk-go-v2/service/sfn v1.18.0/go.mod h1:41VgIwo6R/QE8DnFZ4RrP+f2w9xTzB77h3NRu/BzXyE= -github.com/aws/aws-sdk-go-v2/service/sns v1.20.13 h1:+ADGcDhddHTKyu6Qp3oZKootryteS7D3ODo2ZPDBgjQ= -github.com/aws/aws-sdk-go-v2/service/sns v1.20.13/go.mod h1:rWrvp9i8y/lX94lS7Kn/0iu9RY6vXzeKRqS/knVX8/c= -github.com/aws/aws-sdk-go-v2/service/sqs v1.16.0 h1:dzWS4r8E9bA0TesHM40FSAtedwpTVCuTsLI8EziSqyk= -github.com/aws/aws-sdk-go-v2/service/sqs v1.16.0/go.mod h1:IBTQMG8mtyj37OWg7vIXcg714Ntcb/LlYou/rZpvV1k= -github.com/aws/aws-sdk-go-v2/service/sqs v1.23.2 h1:Y2vfLiY3HmaMisuwx6fS2kMRYbajRXXB+9vesGVPseY= -github.com/aws/aws-sdk-go-v2/service/sqs v1.23.2/go.mod h1:TaV67b6JMD1988x/uMDop/JnMFK6v5d4Ru+sDmFg+ww= -github.com/aws/aws-sdk-go-v2/service/ssm v1.24.0 h1:p22U2yL/AeRToERGcZv1R26Yci5VQnWIrpzcZdG54cg= -github.com/aws/aws-sdk-go-v2/service/ssm v1.24.0/go.mod h1:chcyLYBEVRac/7rWJsD6cUHUR2osROwavvNqCplfwog= -github.com/aws/aws-sdk-go-v2/service/sso v1.9.0 h1:1qLJeQGBmNQW3mBNzK2CFmrQNmoXWrscPqsrAaU1aTA= -github.com/aws/aws-sdk-go-v2/service/sso v1.9.0/go.mod h1:vCV4glupK3tR7pw7ks7Y4jYRL86VvxS+g5qk04YeWrU= github.com/aws/aws-sdk-go-v2/service/sso v1.12.12 h1:nneMBM2p79PGWBQovYO/6Xnc2ryRMw3InnDJq1FHkSY= github.com/aws/aws-sdk-go-v2/service/sso v1.12.12/go.mod h1:HuCOxYsF21eKrerARYO6HapNeh9GBNq7fius2AcwodY= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.12 h1:2qTR7IFk7/0IN/adSFhYu9Xthr0zVFTgBrmPldILn80= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.12/go.mod h1:E4VrHCPzmVB/KFXtqBGKb3c8zpbNBgKe3fisDNLAW5w= -github.com/aws/aws-sdk-go-v2/service/sts v1.14.0 h1:ksiDXhvNYg0D2/UFkLejsaz3LqpW5yjNQ8Nx9Sn2c0E= -github.com/aws/aws-sdk-go-v2/service/sts v1.14.0/go.mod h1:u0xMJKDvvfocRjiozsoZglVNXRG19043xzp3r2ivLIk= github.com/aws/aws-sdk-go-v2/service/sts v1.19.2 h1:XFJ2Z6sNUUcAz9poj+245DMkrHE4h2j5I9/xD50RHfE= github.com/aws/aws-sdk-go-v2/service/sts v1.19.2/go.mod h1:dp0yLPsLBOi++WTxzCjA/oZqi6NPIhoR+uF7GeMU9eg= -github.com/aws/aws-sdk-go-v2/service/wafv2 v1.35.1 h1:FtzLuTf9HPECIcKdBMtA16ZwZWOIj/r57Z3QuWuYfqc= -github.com/aws/aws-sdk-go-v2/service/wafv2 v1.35.1/go.mod h1:RBpb9oTsEgAUfyaTAT2hFC83DxtLxj+SQpcbhaXiHnU= -github.com/aws/smithy-go v1.10.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E= -github.com/aws/smithy-go v1.11.2/go.mod h1:3xHYmszWVx2c0kIwQeEVf9uSm4fYZt67FBJnwub1bgM= github.com/aws/smithy-go v1.13.5 h1:hgz0X/DX0dGqTYpGALqXJoRKRj5oQ7150i5FdTePzO8= github.com/aws/smithy-go v1.13.5/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA= github.com/aymanbagabas/go-osc52 v1.0.3 h1:DTwqENW7X9arYimJrPeGZcV0ln14sGMt3pHZspWD+Mg= @@ -162,51 +69,23 @@ github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJ github.com/charmbracelet/lipgloss v0.5.0/go.mod h1:EZLha/HbzEt7cYqdFPovlqy5FZPj0xFhg5SaqxScmgs= github.com/charmbracelet/lipgloss v0.6.0 h1:1StyZB9vBSOyuZxQUcUwGr17JmojPNm87inij9N3wJY= github.com/charmbracelet/lipgloss v0.6.0/go.mod h1:tHh2wr34xcHjC2HCXIlGSG1jaDF0S0atAUvBMP6Ppuk= -github.com/cloudcmds/tamarin v1.0.0 h1:PhrJ74FCUJo24/nIPXnQe9E3WVEIYo4aG58pICOMDBE= -github.com/cloudcmds/tamarin v1.0.0/go.mod h1:U1aHBoAFtJbI9jzgaj8TUo9C6vfzUKzn1OhWKIdigVM= github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw= github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= -github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= -github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= -github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/gofrs/uuid v4.3.1+incompatible h1:0/KbAdpx3UXAx1kEOWHJeOkpbgRFGHVgv+CFIY7dBJI= -github.com/gofrs/uuid v4.3.1+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= -github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= -github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= -github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= -github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= -github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= -github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= -github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg= -github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= -github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= -github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.0.4 h1:r5O6y84qHX/z/HZV40JBdx2obsHz7/uRj5b+CcYEdeY= -github.com/jackc/pgx/v5 v5.0.4/go.mod h1:U0ynklHtgg43fue9Ly30w3OCSTDPlXjig9ghrNGaguQ= -github.com/jackc/pgx/v5 v5.4.1 h1:oKfB/FhuVtit1bBM3zNRRsZ925ZkMN3HXL+LgLUM9lE= -github.com/jackc/pgx/v5 v5.4.1/go.mod h1:q6iHT8uDNXWiFNOlRqJzBTaSH3+2xCXkokxHZC5qWFY= +github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= @@ -214,19 +93,18 @@ github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfC github.com/juju/ansiterm v0.0.0-20210929141451-8b71cc96ebdc h1:ZQrgZFsLzkw7o3CoDzsfBhx0bf/1rVBXrLy8dXKRe8o= github.com/juju/ansiterm v0.0.0-20210929141451-8b71cc96ebdc/go.mod h1:PyXUpnI3olx3bsPcHt98FGPX/KCFZ1Fi+hw1XLI6384= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lmika/events v0.0.0-20200906102219-a2269cd4394e h1:0QkUe2ejnT/i+xbgGylMU1b+XnZponQKiPVNi+C/xgA= github.com/lmika/events v0.0.0-20200906102219-a2269cd4394e/go.mod h1:qtkBmNC9OfD0STtOR9sF55pQchjIfNlC3gzm4n8CrqM= github.com/lmika/go-bubble-table v0.2.2-0.20220616114432-6bbb2995e538 h1:dtMPRNoDqDnnP3HgOvYhswcJVSqdISkYlCtGOjTqg6Q= github.com/lmika/go-bubble-table v0.2.2-0.20220616114432-6bbb2995e538/go.mod h1:0RT1upgKZ6qZ6B1SqseE3wWsPjSQRv/G/HjpYK8jNsg= -github.com/lmika/gopkgs v0.0.0-20211210041137-0dc91e939890 h1:mwl/exYV/WkBMeShqK7q+B2w2r+b0vP1TSA7clBn9kI= -github.com/lmika/gopkgs v0.0.0-20211210041137-0dc91e939890/go.mod h1:FH6OJSvYcJ9xY8CGs9yGgR89kMCK1UimuUQ6kE5YuJQ= github.com/lmika/gopkgs v0.0.0-20240408110817-a02f6fc67d1f h1:tz68Lhc1oR15HVz69IGbtdukdH0x70kBDEvvj5pTXyE= github.com/lmika/gopkgs v0.0.0-20240408110817-a02f6fc67d1f/go.mod h1:zHQvhjGXRro/Xp2C9dbC+ZUpE0gL4GYW75x1lk7hwzI= github.com/lmika/shellwords v0.0.0-20140714114018-ce258dd729fe h1:1UXS/6OFkbi6JrihPykmYO1VtsABB02QQ+YmYYzTY18= @@ -235,11 +113,9 @@ github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69 github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/lunixbochs/vtclean v1.0.0 h1:xu2sLAri4lGiovBDQKxl5mrXyESr3gUr5m5SM5+LVb8= github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= -github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mattn/go-colorable v0.1.10/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= @@ -250,8 +126,6 @@ github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRC github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho= github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70 h1:kMlmsLSbjkikxQJ1IPwaM+7LJ9ltFu/fi8CRzvSnQmA= github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho= @@ -265,88 +139,42 @@ github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0/go.mod h1:Bd5NYQ github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs= github.com/muesli/termenv v0.13.0 h1:wK20DRpJdDX8b7Ek2QfhvqhRQFZ237RGRO0RQ/Iqdy0= github.com/muesli/termenv v0.13.0/go.mod h1:sP1+uffeLaEYpyOTb8pLCUctGcGLnoFjSn4YJK5e2bc= -github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/risor-io/risor v0.8.0 h1:G0fpHMGztvocKVu8egkKNbvLy4Rsjkuk+0zReu2JSn8= -github.com/risor-io/risor v0.8.0/go.mod h1:lvatEIYxs6HL+X/Bm0R+Mq4Z9a5Y036mniw6DwUnqs0= -github.com/risor-io/risor v1.1.1 h1:J8rIZX/0HXhg/t2+QygksvP65XCWhg5QxRZrwZabhxE= -github.com/risor-io/risor v1.1.1/go.mod h1:0UMw7ZMbUKSPFgQyuHCFe7UuBUewBKX4K3By4ba1CBA= -github.com/risor-io/risor v1.4.0 h1:G17pWgq+N06jWvnaJVwos89tC5C4VMjqwGYRrTWleRM= -github.com/risor-io/risor v1.4.0/go.mod h1:+s/FeK0CdsTCCNZsHSp8EJa3u3mMrhqtNGLCv/GcW8Y= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.2 h1:YwD0ulJSJytLpiaWua0sBDusfsCZohxjxzVTYjwxfV8= github.com/rivo/uniseg v0.4.2/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= -github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI= github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= -github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= -github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= -github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= -github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= -github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/tidwall/gjson v1.14.3 h1:9jvXn7olKEHU1S9vwoMGliaT8jq1vJ7IH/n9zD9Dnlw= -github.com/tidwall/gjson v1.14.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= -github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= -github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= -github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= -github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= -github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= -github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI= github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= -github.com/wI2L/jsondiff v0.3.0 h1:iTzQ9u/d86GE9RsBzVHX88f2EA1vQUboHwLhSQFc1s4= -github.com/wI2L/jsondiff v0.3.0/go.mod h1:y1IMzNNjlSsk3IUoJdRJO7VRBtzMvRgyo4Vu0LdHpTc= -github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU= go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= golang.design/x/clipboard v0.6.2 h1:a3Np4qfKnLWwfFJQhUWU3IDeRfmVuqWl+QPtP4CSYGw= golang.design/x/clipboard v0.6.2/go.mod h1:kqBSweBP0/im4SZGGjLrppH0D400Hnfo5WbFKSNK8N4= -golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 h1:Y/gsMcFOcR+6S6f3YeMKl5g+dZMEWqcz5Czj/GWYbkM= -golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= -golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4= golang.org/x/exp v0.0.0-20230108222341-4b8118a2686a h1:tlXy25amD5A7gOfbXdqCGN5k8ESEed/Ee1E5RcrYnqU= golang.org/x/exp v0.0.0-20230108222341-4b8118a2686a/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/exp/shiny v0.0.0-20230213192124-5e25df0256eb h1:gdeQX7xJSkTNF+Sw7++XNIOo4pGL0CjQv3N2Vm1Erxk= golang.org/x/exp/shiny v0.0.0-20230213192124-5e25df0256eb/go.mod h1:UH99kUObWAZkDnWqppdQe5ZhPYESUw8I0zVV1uWBR+0= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190703141733-d6a02ce849c9/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20211028202545-6944b10bf410 h1:hTftEOvwiOq2+O8k2D5/Q7COC7k5Qcrgc2TFURJYnvQ= golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= golang.org/x/image v0.5.0 h1:5JMiNunQeQw++mMOz48/ISeNu3Iweh/JaZU8ZLqHRrI= golang.org/x/image v0.5.0/go.mod h1:FVC7BI/5Ym8R25iw5OLsgshdUBbT1h5jZTpA+mvAdZ4= @@ -362,13 +190,12 @@ golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -385,14 +212,10 @@ golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= -golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.0.0-20220919170432-7a66f970e087 h1:tPwmk4vmvVCMdr98VgL4JH+qZxPL8fqlUOHnyOM8N3w= -golang.org/x/term v0.0.0-20220919170432-7a66f970e087/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -400,8 +223,6 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= -golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= @@ -422,43 +243,9 @@ google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -ucl.lmika.dev v0.0.0-20240427010304-6315afc54287 h1:llPHrjca54duvQx9PgMTFDhOW2VQiVvqV1CEHpO4AnY= -ucl.lmika.dev v0.0.0-20240427010304-6315afc54287/go.mod h1:T6V4jIUxlWvMTgn4J752VDHNA8iyVrEX6v98EvDj8G4= -ucl.lmika.dev v0.0.0-20240501110514-25594c80d273 h1:+JpKw02VTAcOjJw7Q6juun/9hk9ypNSdTRlf+E4M5Nw= -ucl.lmika.dev v0.0.0-20240501110514-25594c80d273/go.mod h1:T6V4jIUxlWvMTgn4J752VDHNA8iyVrEX6v98EvDj8G4= -ucl.lmika.dev v0.0.0-20240504001444-cf3a12bf0d4d h1:OqGmR0Y+OG6aFIOlXy2QwEHtuUNasYCh/6cxHokYQj4= -ucl.lmika.dev v0.0.0-20240504001444-cf3a12bf0d4d/go.mod h1:T6V4jIUxlWvMTgn4J752VDHNA8iyVrEX6v98EvDj8G4= -ucl.lmika.dev v0.0.0-20240504013531-0dc9fd3c3281 h1:/M7phiv/0XVp3wKkOxEnGQysf8+RS6NOaBQZyUEoSsA= -ucl.lmika.dev v0.0.0-20240504013531-0dc9fd3c3281/go.mod h1:T6V4jIUxlWvMTgn4J752VDHNA8iyVrEX6v98EvDj8G4= -ucl.lmika.dev v0.0.0-20250306030053-ad6d002a22e8 h1:vWttdW8GJWcTUQeJFbQHqCHJDLFWQ9nccUTx/lW2v8s= -ucl.lmika.dev v0.0.0-20250306030053-ad6d002a22e8/go.mod h1:FMP2ncSu4UxfvB0iA2zlebwL+1UPCARdyYNOrmi86A4= -ucl.lmika.dev v0.0.0-20250515115457-27b6cc0b92e2 h1:cvguOoQ0HVgLKbHH17ZHvAUFht6HXApLi0o8JOdaaNU= -ucl.lmika.dev v0.0.0-20250515115457-27b6cc0b92e2/go.mod h1:/MMZKm6mOMtnY4I8TYEot4Pc8dKEy+/IAQo1VdpA5EY= -ucl.lmika.dev v0.0.0-20250517003439-109be33d1495 h1:r46r+7T59Drm+in7TEWKCZfFYIM0ZyZ26QjHAbj8Lto= -ucl.lmika.dev v0.0.0-20250517003439-109be33d1495/go.mod h1:/MMZKm6mOMtnY4I8TYEot4Pc8dKEy+/IAQo1VdpA5EY= -ucl.lmika.dev v0.0.0-20250517115116-0f1ceba0902e h1:CQ+qPqI5lYiiEM0tNAr4jS0iMz16bFqOui5mU3AHsCU= -ucl.lmika.dev v0.0.0-20250517115116-0f1ceba0902e/go.mod h1:/MMZKm6mOMtnY4I8TYEot4Pc8dKEy+/IAQo1VdpA5EY= -ucl.lmika.dev v0.0.0-20250517212052-51e35aa9a675 h1:kGKh3zj6lMzOrGAquFW7ROgx9/6nwJ8DXiSLtceRiak= -ucl.lmika.dev v0.0.0-20250517212052-51e35aa9a675/go.mod h1:/MMZKm6mOMtnY4I8TYEot4Pc8dKEy+/IAQo1VdpA5EY= -ucl.lmika.dev v0.0.0-20250517212757-33d04ba18db4 h1:rnietWu2B+NXLqKfo7jgf6r+srMwxFa5eizywkq4LFk= -ucl.lmika.dev v0.0.0-20250517212757-33d04ba18db4/go.mod h1:/MMZKm6mOMtnY4I8TYEot4Pc8dKEy+/IAQo1VdpA5EY= -ucl.lmika.dev v0.0.0-20250517213937-94aad417121d h1:CMcA8aQV6iiPK75EbHvoIVZhZmSggfrWNhK9BFm2aIg= -ucl.lmika.dev v0.0.0-20250517213937-94aad417121d/go.mod h1:/MMZKm6mOMtnY4I8TYEot4Pc8dKEy+/IAQo1VdpA5EY= -ucl.lmika.dev v0.0.0-20250518024533-f4be44fcbc94 h1:x3IRtT1jbedblimi2hesKGBihg243+wNOSvagCPR0KU= -ucl.lmika.dev v0.0.0-20250518024533-f4be44fcbc94/go.mod h1:/MMZKm6mOMtnY4I8TYEot4Pc8dKEy+/IAQo1VdpA5EY= -ucl.lmika.dev v0.0.0-20250518033831-f79e91e26d78 h1:lbOZUb6whYMLI4win5QL+eLSgqc3N9TtTgT8hTipNl8= -ucl.lmika.dev v0.0.0-20250518033831-f79e91e26d78/go.mod h1:/MMZKm6mOMtnY4I8TYEot4Pc8dKEy+/IAQo1VdpA5EY= -ucl.lmika.dev v0.0.0-20250519111943-1173d163f5e3 h1:ZMQ1rkcAWa///c3bVvlXbtuqjfAWxDm01abQl3g/YVw= -ucl.lmika.dev v0.0.0-20250519111943-1173d163f5e3/go.mod h1:/MMZKm6mOMtnY4I8TYEot4Pc8dKEy+/IAQo1VdpA5EY= -ucl.lmika.dev v0.0.0-20250519114239-7ca821016e9a h1:dzBBFCY50+MQcJaQ90swdDyjzag5oIhwdfqbmZkvX3Q= -ucl.lmika.dev v0.0.0-20250519114239-7ca821016e9a/go.mod h1:/MMZKm6mOMtnY4I8TYEot4Pc8dKEy+/IAQo1VdpA5EY= -ucl.lmika.dev v0.0.0-20250519120409-53b05b5ba6f8 h1:h32JQi0d1MI86RaAMaEU7kvti4uSLX5XYe/nk2abApg= -ucl.lmika.dev v0.0.0-20250519120409-53b05b5ba6f8/go.mod h1:/MMZKm6mOMtnY4I8TYEot4Pc8dKEy+/IAQo1VdpA5EY= ucl.lmika.dev v0.0.0-20250525023717-3076897eb73e h1:N+HzQUunDUvdjAzbSDtHQZVZ1k+XHbVgbNwmc+EKmlQ= ucl.lmika.dev v0.0.0-20250525023717-3076897eb73e/go.mod h1:/MMZKm6mOMtnY4I8TYEot4Pc8dKEy+/IAQo1VdpA5EY= diff --git a/internal/common/ui/commandctrl/cmdpacks/stdcmds.go b/internal/common/ui/commandctrl/cmdpacks/stdcmds.go index 088c0f9..748aaea 100644 --- a/internal/common/ui/commandctrl/cmdpacks/stdcmds.go +++ b/internal/common/ui/commandctrl/cmdpacks/stdcmds.go @@ -417,6 +417,13 @@ func (sc StandardCommands) ConfigureUCL(ucl *ucl.Inst) { ucl.SetBuiltin("noisy-touch", sc.cmdNoisyTouch) ucl.SetBuiltin("rebind", sc.cmdRebind) + // Aliases + ucl.SetBuiltin("sa", sc.cmdSetAttr) + ucl.SetBuiltin("da", sc.cmdDelAttr) + ucl.SetBuiltin("np", sc.cmdNextPage) + ucl.SetBuiltin("w", sc.cmdPut) + ucl.SetBuiltin("q", sc.cmdQuit) + ucl.SetPseudoVar("resultset", resultSetPVar{sc.State, sc.ReadController}) ucl.SetPseudoVar("table", tablePVar{sc.State}) ucl.SetPseudoVar("item", itemPVar{sc.State}) diff --git a/internal/common/ui/commandctrl/cmdpacks/stdcmds_test.go b/internal/common/ui/commandctrl/cmdpacks/stdcmds_test.go index 55bef6e..63f099f 100644 --- a/internal/common/ui/commandctrl/cmdpacks/stdcmds_test.go +++ b/internal/common/ui/commandctrl/cmdpacks/stdcmds_test.go @@ -126,7 +126,6 @@ func newService(t *testing.T, opts ...serviceOpt) *services { inputHistoryService, eventBus, pasteboardprovider.NilProvider{}, - nil, s.table, ) writeController := controllers.NewTableWriteController(state, service, jobsController, readController, settingStore) diff --git a/internal/dynamo-browse/controllers/scripts.go b/internal/dynamo-browse/controllers/scripts.go deleted file mode 100644 index ff2b8d6..0000000 --- a/internal/dynamo-browse/controllers/scripts.go +++ /dev/null @@ -1,290 +0,0 @@ -package controllers - -import ( - "context" - "fmt" - "log" - "strings" - "ucl.lmika.dev/ucl" - - tea "github.com/charmbracelet/bubbletea" - "github.com/lmika/dynamo-browse/internal/common/ui/commandctrl" - "github.com/lmika/dynamo-browse/internal/common/ui/events" - "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" - "github.com/lmika/dynamo-browse/internal/dynamo-browse/models/queryexpr" - "github.com/lmika/dynamo-browse/internal/dynamo-browse/models/relitems" - "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/scriptmanager" - bus "github.com/lmika/events" - "github.com/pkg/errors" -) - -type ScriptController struct { - scriptManager *scriptmanager.Service - tableReadController *TableReadController - jobController *JobsController - settingsController *SettingsController - eventBus *bus.Bus - sendMsg func(msg tea.Msg) -} - -func NewScriptController( - scriptManager *scriptmanager.Service, - tableReadController *TableReadController, - jobController *JobsController, - settingsController *SettingsController, - eventBus *bus.Bus, -) *ScriptController { - sc := &ScriptController{ - scriptManager: scriptManager, - tableReadController: tableReadController, - jobController: jobController, - settingsController: settingsController, - eventBus: eventBus, - } - - sessionImpl := &sessionImpl{sc: sc, lastSelectedItemIndex: -1} - scriptManager.SetIFaces(scriptmanager.Ifaces{ - UI: &uiImpl{sc: sc}, - Session: sessionImpl, - }) - - sessionImpl.subscribeToEvents(eventBus) - - // Setup event handling when settings have changed - eventBus.On(BusEventSettingsUpdated, func(name, value string) { - if !strings.HasPrefix(name, "script.") { - return - } - sc.Init() - }) - - return sc -} - -func (sc *ScriptController) Init() { - if lookupPaths, err := sc.settingsController.settings.ScriptLookupFS(); err == nil { - sc.scriptManager.SetLookupPaths(lookupPaths) - } else { - log.Printf("warn: script lookup paths are invalid: %v", err) - } -} - -func (sc *ScriptController) SetMessageSender(sendMsg func(msg tea.Msg)) { - sc.sendMsg = sendMsg -} - -func (sc *ScriptController) LoadScript(filename string) tea.Msg { - ctx := context.Background() - plugin, err := sc.scriptManager.LoadScript(ctx, filename) - if err != nil { - return events.Error(err) - } - - return events.StatusMsg(fmt.Sprintf("Script '%v' loaded", plugin.Name())) -} - -func (sc *ScriptController) RunScript(filename string) tea.Msg { - ctx := context.Background() - if err := sc.scriptManager.StartAdHocScript(ctx, filename, sc.waitAndPrintScriptError()); err != nil { - return events.Error(err) - } - return nil -} - -func (sc *ScriptController) waitAndPrintScriptError() chan error { - errChan := make(chan error) - go func() { - if err := <-errChan; err != nil { - sc.sendMsg(events.Error(err)) - } - }() - return errChan -} - -func (sc *ScriptController) LookupCommand(name string) commandctrl.Command { - cmd := sc.scriptManager.LookupCommand(name) - if cmd == nil { - return nil - } - - return func(execCtx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg { - errChan := sc.waitAndPrintScriptError() - ctx := context.Background() - - invokeArgs := make([]string, 0) - for args.NArgs() > 0 { - var s string - if err := args.Bind(&s); err == nil { - invokeArgs = append(invokeArgs, s) - } - } - - if err := cmd.Invoke(ctx, invokeArgs, errChan); err != nil { - return events.Error(err) - } - return nil - } -} - -type uiImpl struct { - sc *ScriptController -} - -func (u uiImpl) PrintMessage(ctx context.Context, msg string) { - u.sc.sendMsg(events.StatusMsg(msg)) -} - -func (u uiImpl) Prompt(ctx context.Context, msg string) chan string { - resultChan := make(chan string) - u.sc.sendMsg(events.PromptForInputMsg{ - Prompt: msg, - OnDone: func(value string) tea.Msg { - resultChan <- value - return nil - }, - OnCancel: func() tea.Msg { - close(resultChan) - return nil - }, - }) - return resultChan -} - -type sessionImpl struct { - sc *ScriptController - lastSelectedItemIndex int -} - -func (s *sessionImpl) subscribeToEvents(bus *bus.Bus) { - bus.On("ui.new-item-selected", func(rs *models.ResultSet, itemIndex int) { - s.lastSelectedItemIndex = itemIndex - }) -} - -func (s *sessionImpl) SelectedItemIndex(ctx context.Context) int { - return s.lastSelectedItemIndex -} - -func (s *sessionImpl) ResultSet(ctx context.Context) *models.ResultSet { - return s.sc.tableReadController.state.ResultSet() -} - -func (s *sessionImpl) SetResultSet(ctx context.Context, newResultSet *models.ResultSet) { - state := s.sc.tableReadController.state - msg := s.sc.tableReadController.setResultSetAndFilter(newResultSet, state.filter, true, resultSetUpdateScript) - s.sc.sendMsg(msg) -} - -func (s *sessionImpl) Query(ctx context.Context, query string, opts scriptmanager.QueryOptions) (*models.ResultSet, error) { - // Parse the query - expr, err := queryexpr.Parse(query) - if err != nil { - return nil, err - } - - if opts.NamePlaceholders != nil { - expr = expr.WithNameParams(opts.NamePlaceholders) - } - if opts.ValuePlaceholders != nil { - expr = expr.WithValueParams(opts.ValuePlaceholders) - } - if opts.IndexName != "" { - expr = expr.WithIndex(opts.IndexName) - } - - return s.sc.doQuery(ctx, expr, opts) -} - -func (s *ScriptController) doQuery(ctx context.Context, expr *queryexpr.QueryExpr, opts scriptmanager.QueryOptions) (*models.ResultSet, error) { - // Get the table info - var ( - tableInfo *models.TableInfo - err error - ) - - tableName := opts.TableName - currentResultSet := s.tableReadController.state.ResultSet() - - if tableName != "" { - // Table specified. If it's the same as the current table, then use the existing table info - if currentResultSet != nil && currentResultSet.TableInfo.Name == tableName { - tableInfo = currentResultSet.TableInfo - } - - // Otherwise, describe the table - tableInfo, err = s.tableReadController.tableService.Describe(ctx, tableName) - if err != nil { - return nil, errors.Wrapf(err, "cannot describe table '%v'", tableName) - } - } else { - // Table not specified. Use the existing table, if any - if currentResultSet == nil { - return nil, errors.New("no table currently selected") - } - tableInfo = currentResultSet.TableInfo - } - - newResultSet, err := s.tableReadController.tableService.ScanOrQuery(ctx, tableInfo, expr, nil) - if err != nil { - return nil, err - } - return newResultSet, nil -} - -func (sc *ScriptController) CustomKeyCommand(key string) tea.Cmd { - _, cmd := sc.scriptManager.LookupKeyBinding(key) - if cmd == nil { - return nil - } - - return func() tea.Msg { - errChan := sc.waitAndPrintScriptError() - ctx := context.Background() - - if err := cmd.Invoke(ctx, nil, errChan); err != nil { - return events.Error(err) - } - return nil - } -} - -func (sc *ScriptController) Rebind(bindingName string, newKey string) error { - return sc.scriptManager.RebindKeyBinding(bindingName, newKey) -} - -func (sc *ScriptController) LookupBinding(theKey string) string { - bindingName, _ := sc.scriptManager.LookupKeyBinding(theKey) - return bindingName -} - -func (sc *ScriptController) UnbindKey(key string) { - sc.scriptManager.UnbindKey(key) -} - -func (c *ScriptController) LookupRelatedItems(idx int) (res tea.Msg) { - rs := c.tableReadController.state.ResultSet() - - relItems, err := c.scriptManager.RelatedItemOfItem(context.Background(), rs, idx) - if err != nil { - return events.Error(err) - } else if len(relItems) == 0 { - return events.StatusMsg("No related items available") - } - - return ShowRelatedItemsOverlay{ - Items: relItems, - OnSelected: func(item relitems.RelatedItem) tea.Msg { - if item.OnSelect != nil { - return item.OnSelect() - } - - return NewJob(c.jobController, "Running query…", func(ctx context.Context) (*models.ResultSet, error) { - return c.doQuery(ctx, item.Query, scriptmanager.QueryOptions{ - TableName: item.Table, - }) - }).OnDone(func(rs *models.ResultSet) tea.Msg { - return c.tableReadController.setResultSetAndFilter(rs, "", true, resultSetUpdateQuery) - }).Submit() - }, - } -} diff --git a/internal/dynamo-browse/controllers/scripts_test.go b/internal/dynamo-browse/controllers/scripts_test.go deleted file mode 100644 index ffcb8a8..0000000 --- a/internal/dynamo-browse/controllers/scripts_test.go +++ /dev/null @@ -1,192 +0,0 @@ -package controllers_test - -import ( - "testing" - "time" - - "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" - "github.com/lmika/dynamo-browse/internal/common/ui/events" - "github.com/lmika/dynamo-browse/internal/dynamo-browse/controllers" - "github.com/stretchr/testify/assert" -) - -func TestScriptController_RunScript(t *testing.T) { - t.Run("should execute scripts successfully", func(t *testing.T) { - srv := newService(t, serviceConfig{ - scriptFS: testScriptFile(t, "test.tm", ` - ui.print("Hello world") - `), - }) - - msg := srv.scriptController.RunScript("test.tm") - assert.Nil(t, msg) - - srv.msgSender.waitForAtLeastOneMessages(t, 5*time.Second) - - assert.Len(t, srv.msgSender.msgs, 1) - assert.Equal(t, events.StatusMsg("Hello world"), srv.msgSender.msgs[0]) - }) - - t.Run("session.result_set", func(t *testing.T) { - t.Run("should return current result set if not-nil", func(t *testing.T) { - srv := newService(t, serviceConfig{ - tableName: "alpha-table", - scriptFS: testScriptFile(t, "test.tm", ` - rs := session.result_set() - ui.print(rs.length) - `), - }) - - invokeCommand(t, srv.readController.Init()) - - msg := srv.scriptController.RunScript("test.tm") - assert.Nil(t, msg) - - srv.msgSender.waitForAtLeastOneMessages(t, 5*time.Second) - - assert.Len(t, srv.msgSender.msgs, 1) - assert.Equal(t, events.StatusMsg("3"), srv.msgSender.msgs[0]) - }) - }) - - t.Run("session.query", func(t *testing.T) { - t.Run("should run query against current table", func(t *testing.T) { - srv := newService(t, serviceConfig{ - tableName: "alpha-table", - scriptFS: testScriptFile(t, "test.tm", ` - rs := session.query('pk="abc"') - ui.print(rs.length) - `), - }) - - invokeCommand(t, srv.readController.Init()) - msg := srv.scriptController.RunScript("test.tm") - assert.Nil(t, msg) - - srv.msgSender.waitForAtLeastOneMessages(t, 5*time.Second) - - assert.Len(t, srv.msgSender.msgs, 1) - assert.Equal(t, events.StatusMsg("2"), srv.msgSender.msgs[0]) - }) - - t.Run("should run query against another table", func(t *testing.T) { - srv := newService(t, serviceConfig{ - tableName: "alpha-table", - scriptFS: testScriptFile(t, "test.tm", ` - rs := session.query('pk!="abc"', { table: "count-to-30" }) - ui.print(rs.length) - `), - }) - - invokeCommand(t, srv.readController.Init()) - msg := srv.scriptController.RunScript("test.tm") - assert.Nil(t, msg) - - srv.msgSender.waitForAtLeastOneMessages(t, 5*time.Second) - - assert.Len(t, srv.msgSender.msgs, 1) - assert.Equal(t, events.StatusMsg("30"), srv.msgSender.msgs[0]) - }) - }) - - t.Run("session.set_result_set", func(t *testing.T) { - t.Run("should set the result set from the result of a query", func(t *testing.T) { - srv := newService(t, serviceConfig{ - tableName: "alpha-table", - scriptFS: testScriptFile(t, "test.tm", ` - rs := session.query('pk="abc"') - session.set_result_set(rs) - `), - }) - - invokeCommand(t, srv.readController.Init()) - msg := srv.scriptController.RunScript("test.tm") - assert.Nil(t, msg) - - srv.msgSender.waitForAtLeastOneMessages(t, 5*time.Second) - - assert.Len(t, srv.msgSender.msgs, 1) - assert.IsType(t, controllers.NewResultSet{}, srv.msgSender.msgs[0]) - }) - - t.Run("changed attributes of the result set should show up as modified", func(t *testing.T) { - srv := newService(t, serviceConfig{ - tableName: "alpha-table", - scriptFS: testScriptFile(t, "test.tm", ` - rs := session.query('pk="abc"') - rs[0].set_attr("pk", "131") - session.set_result_set(rs) - `), - }) - - invokeCommand(t, srv.readController.Init()) - msg := srv.scriptController.RunScript("test.tm") - assert.Nil(t, msg) - - srv.msgSender.waitForAtLeastOneMessages(t, 5*time.Second) - - assert.Len(t, srv.msgSender.msgs, 1) - assert.IsType(t, controllers.NewResultSet{}, srv.msgSender.msgs[0]) - - assert.Equal(t, "131", srv.state.ResultSet().Items()[0]["pk"].(*types.AttributeValueMemberS).Value) - assert.True(t, srv.state.ResultSet().IsDirty(0)) - }) - }) -} - -func TestScriptController_LookupCommand(t *testing.T) { - t.Run("should schedule the script on a separate go-routine", func(t *testing.T) { - scenarios := []struct { - descr string - command string - expectedOutput string - }{ - {descr: "command with arg", command: "mycommand \"test name\"", expectedOutput: "Hello, test name"}, - {descr: "command no arg", command: "mycommand", expectedOutput: "Hello, nil value"}, - } - - for _, scenario := range scenarios { - t.Run(scenario.descr, func(t *testing.T) { - srv := newService(t, serviceConfig{ - tableName: "alpha-table", - scriptFS: testScriptFile(t, "test.tm", ` - ext.command("mycommand", func(name = "nil value") { - ui.print(sprintf("Hello, %v", name)) - }) - `), - }) - - invokeCommand(t, srv.scriptController.LoadScript("test.tm")) - invokeCommand(t, srv.commandController.Execute(scenario.command)) - - srv.msgSender.waitForAtLeastOneMessages(t, 5*time.Second) - - assert.Len(t, srv.msgSender.msgs, 1) - assert.Equal(t, events.StatusMsg(scenario.expectedOutput), srv.msgSender.msgs[0]) - }) - } - }) - - t.Run("should only allow one script to run at a time", func(t *testing.T) { - srv := newService(t, serviceConfig{ - tableName: "alpha-table", - scriptFS: testScriptFile(t, "test.tm", ` - ext.command("mycommand", func() { - time.sleep(1.5) - ui.print("Done my thing") - }) - `), - }) - - invokeCommand(t, srv.scriptController.LoadScript("test.tm")) - - invokeCommand(t, srv.commandController.Execute(`mycommand`)) - invokeCommandExpectingError(t, srv.commandController.Execute(`mycommand`)) - - srv.msgSender.waitForAtLeastOneMessages(t, 5*time.Second) - - assert.Len(t, srv.msgSender.msgs, 1) - assert.Equal(t, events.StatusMsg("Done my thing"), srv.msgSender.msgs[0]) - }) - -} diff --git a/internal/dynamo-browse/controllers/tableread.go b/internal/dynamo-browse/controllers/tableread.go index 38946f3..64aca40 100644 --- a/internal/dynamo-browse/controllers/tableread.go +++ b/internal/dynamo-browse/controllers/tableread.go @@ -61,7 +61,6 @@ type TableReadController struct { tableName string loadFromLastView bool pasteboardProvider services.PasteboardProvider - relatedItemSupplier RelatedItemSupplier // state mutex *sync.Mutex @@ -77,7 +76,6 @@ func NewTableReadController( inputHistoryService *inputhistory.Service, eventBus *bus.Bus, pasteboardProvider services.PasteboardProvider, - relatedItemSupplier RelatedItemSupplier, tableName string, ) *TableReadController { return &TableReadController{ @@ -90,7 +88,6 @@ func NewTableReadController( eventBus: eventBus, tableName: tableName, pasteboardProvider: pasteboardProvider, - relatedItemSupplier: relatedItemSupplier, mutex: new(sync.Mutex), } } diff --git a/internal/dynamo-browse/controllers/tablewrite_test.go b/internal/dynamo-browse/controllers/tablewrite_test.go index a407923..0ace3d9 100644 --- a/internal/dynamo-browse/controllers/tablewrite_test.go +++ b/internal/dynamo-browse/controllers/tablewrite_test.go @@ -15,7 +15,6 @@ import ( "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/inputhistory" "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/itemrenderer" "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/jobs" - "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/scriptmanager" "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/tables" "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/viewsnapshot" "github.com/lmika/dynamo-browse/test/testdynamo" @@ -587,7 +586,6 @@ type services struct { settingsController *controllers.SettingsController columnsController *controllers.ColumnsController exportController *controllers.ExportController - scriptController *controllers.ScriptController commandController *commandctrl.CommandController } @@ -607,7 +605,6 @@ func newService(t *testing.T, cfg serviceConfig) *services { workspaceService := viewsnapshot.NewService(resultSetSnapshotStore) itemRendererService := itemrenderer.NewService(itemrenderer.PlainTextRenderer(), itemrenderer.PlainTextRenderer()) - scriptService := scriptmanager.New() inputHistoryService := inputhistory.New(inputHistoryStore) client := testdynamo.SetupTestTable(t, testData) @@ -627,17 +624,14 @@ func newService(t *testing.T, cfg serviceConfig) *services { inputHistoryService, eventBus, pasteboardprovider.NilProvider{}, - nil, cfg.tableName, ) writeController := controllers.NewTableWriteController(state, service, jobsController, readController, settingStore) settingsController := controllers.NewSettingsController(settingStore, eventBus) columnsController := controllers.NewColumnsController(readController, eventBus) exportController := controllers.NewExportController(state, service, jobsController, columnsController, pasteboardprovider.NilProvider{}) - scriptController := controllers.NewScriptController(scriptService, readController, jobsController, settingsController, eventBus) commandController, _ := commandctrl.NewCommandController(inputHistoryService) - commandController.AddCommandLookupExtension(scriptController) if cfg.isReadOnly { if err := settingStore.SetReadOnly(cfg.isReadOnly); err != nil { @@ -651,12 +645,7 @@ func newService(t *testing.T, cfg serviceConfig) *services { } msgSender := &msgSender{} - scriptController.Init() jobsController.SetMessageSender(msgSender.send) - scriptController.SetMessageSender(msgSender.send) - - // Initting will setup the default script lookup paths, so revert them to the test ones - scriptService.SetLookupPaths([]fs.FS{cfg.scriptFS}) return &services{ state: state, @@ -666,7 +655,6 @@ func newService(t *testing.T, cfg serviceConfig) *services { settingsController: settingsController, columnsController: columnsController, exportController: exportController, - scriptController: scriptController, commandController: commandController, msgSender: msgSender, } diff --git a/internal/dynamo-browse/services/scriptmanager/builtins.go b/internal/dynamo-browse/services/scriptmanager/builtins.go deleted file mode 100644 index 93c6e78..0000000 --- a/internal/dynamo-browse/services/scriptmanager/builtins.go +++ /dev/null @@ -1,102 +0,0 @@ -/** - * Builtins adopted and modified from Taramin - * Copyright (c) 2022 Curtis Myzie - */ - -package scriptmanager - -import ( - "context" - "fmt" - "log" - - "github.com/pkg/errors" - "github.com/risor-io/risor/object" -) - -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 -} - -// This is taken from the args package -func require(funcName string, count int, args []object.Object) *object.Error { - nArgs := len(args) - if nArgs != count { - if count == 1 { - return object.Errorf( - fmt.Sprintf("type error: %s() takes exactly 1 argument (%d given)", - funcName, nArgs)) - } - return object.Errorf( - fmt.Sprintf("type error: %s() takes exactly %d arguments (%d given)", - funcName, count, nArgs)) - } - return nil -} - -func bindArgs(funcName string, args []object.Object, bindArgs ...any) *object.Error { - if err := require(funcName, len(bindArgs), args); err != nil { - return err - } - - for i, bindArg := range bindArgs { - switch t := bindArg.(type) { - case *string: - str, err := object.AsString(args[i]) - if err != nil { - return err - } - - *t = str - case **object.Function: - fnRes, isFnRes := args[i].(*object.Function) - if !isFnRes { - return object.NewError(errors.Errorf("expected arg %v to be a function, was %T", i, bindArg)) - } - - *t = fnRes - default: - return object.NewError(errors.Errorf("unhandled arg type %v", i)) - } - } - return nil -} diff --git a/internal/dynamo-browse/services/scriptmanager/iface.go b/internal/dynamo-browse/services/scriptmanager/iface.go deleted file mode 100644 index 39b4d9e..0000000 --- a/internal/dynamo-browse/services/scriptmanager/iface.go +++ /dev/null @@ -1,38 +0,0 @@ -package scriptmanager - -import ( - "context" - "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" - "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" -) - -//go:generate mockery --with-expecter --name UIService -//go:generate mockery --with-expecter --name SessionService - -type Ifaces struct { - UI UIService - Session SessionService -} - -type UIService interface { - PrintMessage(ctx context.Context, msg string) - - // Prompt should return a channel which will provide the input from the user. If the user - // provides no input, prompt should close the channel without providing anything. - Prompt(ctx context.Context, msg string) chan string -} - -type SessionService interface { - Query(ctx context.Context, expr string, queryOptions QueryOptions) (*models.ResultSet, error) - - ResultSet(ctx context.Context) *models.ResultSet - SelectedItemIndex(ctx context.Context) int - SetResultSet(ctx context.Context, newResultSet *models.ResultSet) -} - -type QueryOptions struct { - TableName string - IndexName string - NamePlaceholders map[string]string - ValuePlaceholders map[string]types.AttributeValue -} diff --git a/internal/dynamo-browse/services/scriptmanager/mocks/SessionService.go b/internal/dynamo-browse/services/scriptmanager/mocks/SessionService.go deleted file mode 100644 index bdfa6b1..0000000 --- a/internal/dynamo-browse/services/scriptmanager/mocks/SessionService.go +++ /dev/null @@ -1,216 +0,0 @@ -// Code generated by mockery v2.20.0. DO NOT EDIT. - -package mocks - -import ( - context "context" - - models "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" - mock "github.com/stretchr/testify/mock" - - scriptmanager "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/scriptmanager" -) - -// SessionService is an autogenerated mock type for the SessionService type -type SessionService struct { - mock.Mock -} - -type SessionService_Expecter struct { - mock *mock.Mock -} - -func (_m *SessionService) EXPECT() *SessionService_Expecter { - return &SessionService_Expecter{mock: &_m.Mock} -} - -// Query provides a mock function with given fields: ctx, expr, queryOptions -func (_m *SessionService) Query(ctx context.Context, expr string, queryOptions scriptmanager.QueryOptions) (*models.ResultSet, error) { - ret := _m.Called(ctx, expr, queryOptions) - - var r0 *models.ResultSet - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string, scriptmanager.QueryOptions) (*models.ResultSet, error)); ok { - return rf(ctx, expr, queryOptions) - } - if rf, ok := ret.Get(0).(func(context.Context, string, scriptmanager.QueryOptions) *models.ResultSet); ok { - r0 = rf(ctx, expr, queryOptions) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*models.ResultSet) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, string, scriptmanager.QueryOptions) error); ok { - r1 = rf(ctx, expr, queryOptions) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// SessionService_Query_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Query' -type SessionService_Query_Call struct { - *mock.Call -} - -// Query is a helper method to define mock.On call -// - ctx context.Context -// - expr string -// - queryOptions scriptmanager.QueryOptions -func (_e *SessionService_Expecter) Query(ctx interface{}, expr interface{}, queryOptions interface{}) *SessionService_Query_Call { - return &SessionService_Query_Call{Call: _e.mock.On("Query", ctx, expr, queryOptions)} -} - -func (_c *SessionService_Query_Call) Run(run func(ctx context.Context, expr string, queryOptions scriptmanager.QueryOptions)) *SessionService_Query_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(string), args[2].(scriptmanager.QueryOptions)) - }) - return _c -} - -func (_c *SessionService_Query_Call) Return(_a0 *models.ResultSet, _a1 error) *SessionService_Query_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *SessionService_Query_Call) RunAndReturn(run func(context.Context, string, scriptmanager.QueryOptions) (*models.ResultSet, error)) *SessionService_Query_Call { - _c.Call.Return(run) - return _c -} - -// ResultSet provides a mock function with given fields: ctx -func (_m *SessionService) ResultSet(ctx context.Context) *models.ResultSet { - ret := _m.Called(ctx) - - var r0 *models.ResultSet - if rf, ok := ret.Get(0).(func(context.Context) *models.ResultSet); ok { - r0 = rf(ctx) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*models.ResultSet) - } - } - - return r0 -} - -// SessionService_ResultSet_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ResultSet' -type SessionService_ResultSet_Call struct { - *mock.Call -} - -// ResultSet is a helper method to define mock.On call -// - ctx context.Context -func (_e *SessionService_Expecter) ResultSet(ctx interface{}) *SessionService_ResultSet_Call { - return &SessionService_ResultSet_Call{Call: _e.mock.On("ResultSet", ctx)} -} - -func (_c *SessionService_ResultSet_Call) Run(run func(ctx context.Context)) *SessionService_ResultSet_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context)) - }) - return _c -} - -func (_c *SessionService_ResultSet_Call) Return(_a0 *models.ResultSet) *SessionService_ResultSet_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *SessionService_ResultSet_Call) RunAndReturn(run func(context.Context) *models.ResultSet) *SessionService_ResultSet_Call { - _c.Call.Return(run) - return _c -} - -// SelectedItemIndex provides a mock function with given fields: ctx -func (_m *SessionService) SelectedItemIndex(ctx context.Context) int { - ret := _m.Called(ctx) - - var r0 int - if rf, ok := ret.Get(0).(func(context.Context) int); ok { - r0 = rf(ctx) - } else { - r0 = ret.Get(0).(int) - } - - return r0 -} - -// SessionService_SelectedItemIndex_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SelectedItemIndex' -type SessionService_SelectedItemIndex_Call struct { - *mock.Call -} - -// SelectedItemIndex is a helper method to define mock.On call -// - ctx context.Context -func (_e *SessionService_Expecter) SelectedItemIndex(ctx interface{}) *SessionService_SelectedItemIndex_Call { - return &SessionService_SelectedItemIndex_Call{Call: _e.mock.On("SelectedItemIndex", ctx)} -} - -func (_c *SessionService_SelectedItemIndex_Call) Run(run func(ctx context.Context)) *SessionService_SelectedItemIndex_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context)) - }) - return _c -} - -func (_c *SessionService_SelectedItemIndex_Call) Return(_a0 int) *SessionService_SelectedItemIndex_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *SessionService_SelectedItemIndex_Call) RunAndReturn(run func(context.Context) int) *SessionService_SelectedItemIndex_Call { - _c.Call.Return(run) - return _c -} - -// SetResultSet provides a mock function with given fields: ctx, newResultSet -func (_m *SessionService) SetResultSet(ctx context.Context, newResultSet *models.ResultSet) { - _m.Called(ctx, newResultSet) -} - -// SessionService_SetResultSet_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetResultSet' -type SessionService_SetResultSet_Call struct { - *mock.Call -} - -// SetResultSet is a helper method to define mock.On call -// - ctx context.Context -// - newResultSet *models.ResultSet -func (_e *SessionService_Expecter) SetResultSet(ctx interface{}, newResultSet interface{}) *SessionService_SetResultSet_Call { - return &SessionService_SetResultSet_Call{Call: _e.mock.On("SetResultSet", ctx, newResultSet)} -} - -func (_c *SessionService_SetResultSet_Call) Run(run func(ctx context.Context, newResultSet *models.ResultSet)) *SessionService_SetResultSet_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(*models.ResultSet)) - }) - return _c -} - -func (_c *SessionService_SetResultSet_Call) Return() *SessionService_SetResultSet_Call { - _c.Call.Return() - return _c -} - -func (_c *SessionService_SetResultSet_Call) RunAndReturn(run func(context.Context, *models.ResultSet)) *SessionService_SetResultSet_Call { - _c.Call.Return(run) - return _c -} - -type mockConstructorTestingTNewSessionService interface { - mock.TestingT - Cleanup(func()) -} - -// NewSessionService creates a new instance of SessionService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewSessionService(t mockConstructorTestingTNewSessionService) *SessionService { - mock := &SessionService{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/internal/dynamo-browse/services/scriptmanager/mocks/UIService.go b/internal/dynamo-browse/services/scriptmanager/mocks/UIService.go deleted file mode 100644 index b029dd6..0000000 --- a/internal/dynamo-browse/services/scriptmanager/mocks/UIService.go +++ /dev/null @@ -1,116 +0,0 @@ -// Code generated by mockery v2.20.0. DO NOT EDIT. - -package mocks - -import ( - context "context" - - mock "github.com/stretchr/testify/mock" -) - -// UIService is an autogenerated mock type for the UIService type -type UIService struct { - mock.Mock -} - -type UIService_Expecter struct { - mock *mock.Mock -} - -func (_m *UIService) EXPECT() *UIService_Expecter { - return &UIService_Expecter{mock: &_m.Mock} -} - -// PrintMessage provides a mock function with given fields: ctx, msg -func (_m *UIService) PrintMessage(ctx context.Context, msg string) { - _m.Called(ctx, msg) -} - -// UIService_PrintMessage_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PrintMessage' -type UIService_PrintMessage_Call struct { - *mock.Call -} - -// PrintMessage is a helper method to define mock.On call -// - ctx context.Context -// - msg string -func (_e *UIService_Expecter) PrintMessage(ctx interface{}, msg interface{}) *UIService_PrintMessage_Call { - return &UIService_PrintMessage_Call{Call: _e.mock.On("PrintMessage", ctx, msg)} -} - -func (_c *UIService_PrintMessage_Call) Run(run func(ctx context.Context, msg string)) *UIService_PrintMessage_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(string)) - }) - return _c -} - -func (_c *UIService_PrintMessage_Call) Return() *UIService_PrintMessage_Call { - _c.Call.Return() - return _c -} - -func (_c *UIService_PrintMessage_Call) RunAndReturn(run func(context.Context, string)) *UIService_PrintMessage_Call { - _c.Call.Return(run) - return _c -} - -// Prompt provides a mock function with given fields: ctx, msg -func (_m *UIService) Prompt(ctx context.Context, msg string) chan string { - ret := _m.Called(ctx, msg) - - var r0 chan string - if rf, ok := ret.Get(0).(func(context.Context, string) chan string); ok { - r0 = rf(ctx, msg) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(chan string) - } - } - - return r0 -} - -// UIService_Prompt_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Prompt' -type UIService_Prompt_Call struct { - *mock.Call -} - -// Prompt is a helper method to define mock.On call -// - ctx context.Context -// - msg string -func (_e *UIService_Expecter) Prompt(ctx interface{}, msg interface{}) *UIService_Prompt_Call { - return &UIService_Prompt_Call{Call: _e.mock.On("Prompt", ctx, msg)} -} - -func (_c *UIService_Prompt_Call) Run(run func(ctx context.Context, msg string)) *UIService_Prompt_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(string)) - }) - return _c -} - -func (_c *UIService_Prompt_Call) Return(_a0 chan string) *UIService_Prompt_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *UIService_Prompt_Call) RunAndReturn(run func(context.Context, string) chan string) *UIService_Prompt_Call { - _c.Call.Return(run) - return _c -} - -type mockConstructorTestingTNewUIService interface { - mock.TestingT - Cleanup(func()) -} - -// NewUIService creates a new instance of UIService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewUIService(t mockConstructorTestingTNewUIService) *UIService { - mock := &UIService{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/internal/dynamo-browse/services/scriptmanager/modext.go b/internal/dynamo-browse/services/scriptmanager/modext.go deleted file mode 100644 index 4ad97d4..0000000 --- a/internal/dynamo-browse/services/scriptmanager/modext.go +++ /dev/null @@ -1,270 +0,0 @@ -package scriptmanager - -import ( - "context" - "fmt" - "regexp" - - "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" - "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" - "github.com/lmika/dynamo-browse/internal/dynamo-browse/models/queryexpr" - "github.com/pkg/errors" - "github.com/risor-io/risor/object" -) - -var ( - validKeyBindingNames = regexp.MustCompile(`^[-a-zA-Z0-9_]+$`) -) - -type extModule struct { - scriptPlugin *ScriptPlugin -} - -func (m *extModule) register() *object.Module { - return object.NewBuiltinsModule("ext", map[string]object.Object{ - "command": object.NewBuiltin("command", m.command), - "key_binding": object.NewBuiltin("key_binding", m.keyBinding), - "related_items": object.NewBuiltin("related_items", m.relatedItem), - }) -} - -func (m *extModule) command(ctx context.Context, args ...object.Object) object.Object { - thisEnv := scriptEnvFromCtx(ctx) - - if err := require("ext.command", 2, args); err != nil { - return err - } - - cmdName, err := object.AsString(args[0]) - if err != nil { - return err - } - fnRes, isFnRes := args[1].(*object.Function) - if !isFnRes { - return object.NewError(errors.New("expected second arg to be a function")) - } - - callFn, hasCallFn := object.GetCallFunc(ctx) - if !hasCallFn { - return object.NewError(errors.New("no callFn found in context")) - } - - // This command function will be executed by the script scheduler - newCommand := func(ctx context.Context, args []string) error { - objArgs := make([]object.Object, len(args)) - for i, a := range args { - objArgs[i] = object.NewString(a) - } - - newEnv := thisEnv - ctx = ctxWithScriptEnv(ctx, newEnv) - - res, err := callFn(ctx, fnRes, objArgs) - if err != nil { - return errors.Errorf("command error '%v':%v - %v", m.scriptPlugin.name, cmdName, err) - } else if object.IsError(res) { - errObj := res.(*object.Error) - return errors.Errorf("command error '%v':%v - %v", m.scriptPlugin.name, cmdName, errObj.Inspect()) - } - return nil - } - - if m.scriptPlugin.definedCommands == nil { - m.scriptPlugin.definedCommands = make(map[string]*Command) - } - m.scriptPlugin.definedCommands[cmdName] = &Command{plugin: m.scriptPlugin, cmdFn: newCommand} - return nil -} - -func (m *extModule) keyBinding(ctx context.Context, args ...object.Object) object.Object { - thisEnv := scriptEnvFromCtx(ctx) - - if err := require("ext.key_binding", 3, args); err != nil { - return err - } - - bindingName, err := object.AsString(args[0]) - if err != nil { - return err - } else if !validKeyBindingNames.MatchString(bindingName) { - return object.NewError(errors.New("value error: binding name must match regexp [-a-zA-Z0-9_]+")) - } - - options, err := object.AsMap(args[1]) - if err != nil { - return err - } - - var defaultKey string - if strVal, isStrVal := options.Get("default").(*object.String); isStrVal { - defaultKey = strVal.Value() - } - - fnRes, isFnRes := args[2].(*object.Function) - if !isFnRes { - return object.NewError(errors.New("expected second arg to be a function")) - } - - callFn, hasCallFn := object.GetCallFunc(ctx) - if !hasCallFn { - return object.NewError(errors.New("no callFn found in context")) - } - - // This command function will be executed by the script scheduler - newCommand := func(ctx context.Context, args []string) error { - objArgs := make([]object.Object, len(args)) - for i, a := range args { - objArgs[i] = object.NewString(a) - } - - newEnv := thisEnv - ctx = ctxWithScriptEnv(ctx, newEnv) - - res, err := callFn(ctx, fnRes, objArgs) - if err != nil { - return errors.Errorf("command error '%v':%v - %v", m.scriptPlugin.name, bindingName, err) - } else if object.IsError(res) { - errObj := res.(*object.Error) - return errors.Errorf("command error '%v':%v - %v", m.scriptPlugin.name, bindingName, errObj.Inspect()) - } - return nil - } - - fullBindingName := fmt.Sprintf("ext.%v.%v", m.scriptPlugin.name, bindingName) - - if m.scriptPlugin.definedKeyBindings == nil { - m.scriptPlugin.definedKeyBindings = make(map[string]*Command) - m.scriptPlugin.keyToKeyBinding = make(map[string]string) - } - - m.scriptPlugin.definedKeyBindings[fullBindingName] = &Command{plugin: m.scriptPlugin, cmdFn: newCommand} - m.scriptPlugin.keyToKeyBinding[defaultKey] = fullBindingName - return nil -} - -func (m *extModule) relatedItem(ctx context.Context, args ...object.Object) object.Object { - thisEnv := scriptEnvFromCtx(ctx) - - var ( - tableName string - callbackFn *object.Function - ) - if err := bindArgs("ext.related_items", args, &tableName, &callbackFn); err != nil { - return err - } - - callFn, hasCallFn := object.GetCallFunc(ctx) - if !hasCallFn { - return object.NewError(errors.New("no callFn found in context")) - } - - newHandler := func(ctx context.Context, rs *models.ResultSet, index int) ([]relatedItem, error) { - newEnv := thisEnv - ctx = ctxWithScriptEnv(ctx, newEnv) - - res, err := callFn(ctx, callbackFn, []object.Object{ - newItemProxy(newResultSetProxy(rs), index), - }) - - if err != nil { - return nil, errors.Errorf("script error '%v':related_item - %v", m.scriptPlugin.name, err) - } else if object.IsError(res) { - errObj := res.(*object.Error) - return nil, errors.Errorf("script error '%v':related_item - %v", m.scriptPlugin.name, errObj.Inspect()) - } - - itr, objErr := object.AsIterator(res) - if err != nil { - return nil, objErr.Value() - } - - var relItems []relatedItem - for next, hasNext := itr.Next(ctx); hasNext; next, hasNext = itr.Next(ctx) { - var newRelItem relatedItem - - itemMap, objErr := object.AsMap(next) - if err != nil { - return nil, objErr.Value() - } - - labelName, objErr := object.AsString(itemMap.Get("label")) - if objErr != nil { - continue - } - newRelItem.label = labelName - - var tableStr = "" - if itemMap.Get("table") != object.Nil { - tableStr, objErr = object.AsString(itemMap.Get("table")) - if objErr != nil { - continue - } - } - newRelItem.table = tableStr - - if selectFn, ok := itemMap.Get("on_select").(*object.Function); ok { - newRelItem.onSelect = func() error { - thisNewEnv := thisEnv - ctx = ctxWithScriptEnv(ctx, thisNewEnv) - - res, err := callFn(ctx, selectFn, []object.Object{}) - if err != nil { - return errors.Errorf("rel error '%v' - %v", m.scriptPlugin.name, err) - } else if object.IsError(res) { - errObj := res.(*object.Error) - return errors.Errorf("rel error '%v' - %v", m.scriptPlugin.name, errObj.Inspect()) - } - return nil - } - } else { - queryExprStr, objErr := object.AsString(itemMap.Get("query")) - if objErr != nil { - continue - } - - query, err := queryexpr.Parse(queryExprStr) - if err != nil { - continue - } - - // Placeholders - if argsVal, isArgsValMap := object.AsMap(itemMap.Get("args")); isArgsValMap == nil { - namePlaceholders := make(map[string]string) - valuePlaceholders := make(map[string]types.AttributeValue) - - for k, val := range argsVal.Value() { - switch v := val.(type) { - case *object.String: - namePlaceholders[k] = v.Value() - valuePlaceholders[k] = &types.AttributeValueMemberS{Value: v.Value()} - case *object.Int: - valuePlaceholders[k] = &types.AttributeValueMemberN{Value: fmt.Sprint(v.Value())} - case *object.Float: - valuePlaceholders[k] = &types.AttributeValueMemberN{Value: fmt.Sprint(v.Value())} - case *object.Bool: - valuePlaceholders[k] = &types.AttributeValueMemberBOOL{Value: v.Value()} - case *object.NilType: - valuePlaceholders[k] = &types.AttributeValueMemberNULL{Value: true} - default: - continue - } - } - - query = query.WithNameParams(namePlaceholders).WithValueParams(valuePlaceholders) - } - newRelItem.query = query - } - - relItems = append(relItems, newRelItem) - } - - return relItems, nil - } - - m.scriptPlugin.relatedItems = append(m.scriptPlugin.relatedItems, &relatedItemBuilder{ - table: tableName, - itemProduction: newHandler, - }) - - return nil -} diff --git a/internal/dynamo-browse/services/scriptmanager/modext_test.go b/internal/dynamo-browse/services/scriptmanager/modext_test.go deleted file mode 100644 index b3413e6..0000000 --- a/internal/dynamo-browse/services/scriptmanager/modext_test.go +++ /dev/null @@ -1,151 +0,0 @@ -package scriptmanager_test - -import ( - "context" - "testing" - - "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" - "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" - "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/scriptmanager" - "github.com/stretchr/testify/assert" -) - -func TestExtModule_RelatedItems(t *testing.T) { - t.Run("should register a function which will return related items for an item", func(t *testing.T) { - scenarios := []struct { - desc string - code string - }{ - { - desc: "single function, table name match", - code: ` - ext.related_items("test-table", func(item) { - print("Hello") - return [ - {"label": "Customer", "query": "pk=$foo", "args": {"foo": "foo"}}, - {"label": "Payment", "query": "fla=$daa", "args": {"daa": "Hello"}}, - ] - }) - `, - }, - { - desc: "single function, table prefix match", - code: ` - ext.related_items("test-*", func(item) { - print("Hello") - return [ - {"label": "Customer", "query": "pk=$foo", "args": {"foo": "foo"}}, - {"label": "Payment", "query": "fla=$daa", "args": {"daa": "Hello"}}, - ] - }) - `, - }, - { - desc: "multi function, table name match", - code: ` - ext.related_items("test-table", func(item) { - print("Hello") - return [ - {"label": "Customer", "query": "pk=$foo", "args": {"foo": "foo"}}, - ] - }) - - ext.related_items("test-table", func(item) { - return [ - {"label": "Payment", "query": "fla=$daa", "args": {"daa": "Hello"}}, - ] - }) - `, - }, - { - desc: "multi function, table name prefix", - code: ` - ext.related_items("test-*", func(item) { - print("Hello") - return [ - {"label": "Customer", "query": "pk=$foo", "args": {"foo": "foo"}}, - ] - }) - - ext.related_items("test-*", func(item) { - return [ - {"label": "Payment", "query": "fla=$daa", "args": {"daa": "Hello"}}, - ] - }) - `, - }, - } - - for _, scenario := range scenarios { - t.Run(scenario.desc, func(t *testing.T) { - // Load the script - srv := scriptmanager.New(scriptmanager.WithFS(testScriptFile(t, "test.tm", scenario.code))) - - ctx := context.Background() - plugin, err := srv.LoadScript(ctx, "test.tm") - assert.NoError(t, err) - assert.NotNil(t, plugin) - - // Get related items of result set - rs := &models.ResultSet{ - TableInfo: &models.TableInfo{ - Name: "test-table", - }, - } - rs.SetItems([]models.Item{ - {"pk": &types.AttributeValueMemberS{Value: "abc"}}, - {"pk": &types.AttributeValueMemberS{Value: "1232"}}, - }) - - relItems, err := srv.RelatedItemOfItem(context.Background(), rs, 0) - assert.NoError(t, err) - assert.Len(t, relItems, 2) - - assert.Equal(t, "Customer", relItems[0].Name) - assert.Equal(t, "pk=$foo", relItems[0].Query.String()) - assert.Equal(t, "foo", relItems[0].Query.ValueParamOrNil("foo").(*types.AttributeValueMemberS).Value) - - assert.Equal(t, "Payment", relItems[1].Name) - assert.Equal(t, "fla=$daa", relItems[1].Query.String()) - assert.Equal(t, "Hello", relItems[1].Query.ValueParamOrNil("daa").(*types.AttributeValueMemberS).Value) - }) - } - }) - - t.Run("should support rel_items with on select", func(t *testing.T) { - // Load the script - srv := scriptmanager.New(scriptmanager.WithFS(testScriptFile(t, "test.tm", ` - ext.related_items("test-table", func(item) { - print("Hello") - return [ - {"label": "Customer", "on_select": func() { - print("Selected") - }}, - ] - }) - `))) - - ctx := context.Background() - plugin, err := srv.LoadScript(ctx, "test.tm") - assert.NoError(t, err) - assert.NotNil(t, plugin) - - // Get related items of result set - rs := &models.ResultSet{ - TableInfo: &models.TableInfo{ - Name: "test-table", - }, - } - rs.SetItems([]models.Item{ - {"pk": &types.AttributeValueMemberS{Value: "abc"}}, - {"pk": &types.AttributeValueMemberS{Value: "1232"}}, - }) - - relItems, err := srv.RelatedItemOfItem(context.Background(), rs, 0) - assert.NoError(t, err) - assert.Len(t, relItems, 1) - - assert.Equal(t, "Customer", relItems[0].Name) - assert.NoError(t, relItems[0].OnSelect()) - }) -} diff --git a/internal/dynamo-browse/services/scriptmanager/modos_test.go b/internal/dynamo-browse/services/scriptmanager/modos_test.go deleted file mode 100644 index 455125b..0000000 --- a/internal/dynamo-browse/services/scriptmanager/modos_test.go +++ /dev/null @@ -1,56 +0,0 @@ -package scriptmanager_test - -import ( - "context" - "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/scriptmanager" - "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/scriptmanager/mocks" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "testing" -) - -func TestOSModule_Env(t *testing.T) { - t.Run("should return value of environment variables", func(t *testing.T) { - t.Setenv("FULL_VALUE", "this is a value") - t.Setenv("EMPTY_VALUE", "") - - testFS := testScriptFile(t, "test.tm", ` - assert(os.getenv("FULL_VALUE") == "this is a value") - assert(os.getenv("EMPTY_VALUE") == "") - assert(os.getenv("MISSING_VALUE") == "") - - assert(bool(os.getenv("FULL_VALUE")) == true) - assert(bool(os.getenv("EMPTY_VALUE")) == false) - assert(bool(os.getenv("MISSING_VALUE")) == false) - `) - - srv := scriptmanager.New(scriptmanager.WithFS(testFS)) - - ctx := context.Background() - err := <-srv.RunAdHocScript(ctx, "test.tm") - assert.NoError(t, err) - }) -} - -func TestOSModule_Exec(t *testing.T) { - t.Run("should run command and return stdout", func(t *testing.T) { - mockedUIService := mocks.NewUIService(t) - mockedUIService.EXPECT().PrintMessage(mock.Anything, "hello world\n") - - testFS := testScriptFile(t, "test.tm", ` - res := exec('echo', ["hello world"]).stdout - ui.print(res) - `) - - srv := scriptmanager.New(scriptmanager.WithFS(testFS)) - srv.SetIFaces(scriptmanager.Ifaces{ - UI: mockedUIService, - }) - - ctx := context.Background() - err := <-srv.RunAdHocScript(ctx, "test.tm") - assert.NoError(t, err) - - mockedUIService.AssertExpectations(t) - }) -} diff --git a/internal/dynamo-browse/services/scriptmanager/modsession.go b/internal/dynamo-browse/services/scriptmanager/modsession.go deleted file mode 100644 index 95c16c7..0000000 --- a/internal/dynamo-browse/services/scriptmanager/modsession.go +++ /dev/null @@ -1,145 +0,0 @@ -package scriptmanager - -import ( - "context" - "fmt" - "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" - "github.com/pkg/errors" - "github.com/risor-io/risor/object" -) - -type sessionModule struct { - sessionService SessionService -} - -func (um *sessionModule) query(ctx context.Context, args ...object.Object) object.Object { - if len(args) == 0 || len(args) > 2 { - return object.Errorf("type error: session.query takes either 1 or 2 arguments (%d given)", len(args)) - } - - var options QueryOptions - - expr, objErr := object.AsString(args[0]) - if objErr != nil { - return objErr - } - - if len(args) == 2 { - objMap, objErr := object.AsMap(args[1]) - if objErr != nil { - return objErr - } - - // Table name - if val := objMap.Get("table"); val != object.Nil && val.IsTruthy() { - switch tv := val.(type) { - case *object.String: - options.TableName = tv.Value() - case *tableProxy: - options.TableName = tv.table.Name - default: - return object.Errorf("type error: query option 'table' must be either a string or table") - } - } - - // Index name - if val, isStr := objMap.Get("index").(*object.String); isStr { - options.IndexName = val.Value() - } - - // Placeholders - if argsVal, isArgsValMap := objMap.Get("args").(*object.Map); isArgsValMap { - options.NamePlaceholders = make(map[string]string) - options.ValuePlaceholders = make(map[string]types.AttributeValue) - - for k, val := range argsVal.Value() { - switch v := val.(type) { - case *object.String: - options.NamePlaceholders[k] = v.Value() - options.ValuePlaceholders[k] = &types.AttributeValueMemberS{Value: v.Value()} - case *object.Int: - options.ValuePlaceholders[k] = &types.AttributeValueMemberN{Value: fmt.Sprint(v.Value())} - case *object.Float: - options.ValuePlaceholders[k] = &types.AttributeValueMemberN{Value: fmt.Sprint(v.Value())} - case *object.Bool: - options.ValuePlaceholders[k] = &types.AttributeValueMemberBOOL{Value: v.Value()} - case *object.NilType: - options.ValuePlaceholders[k] = &types.AttributeValueMemberNULL{Value: true} - default: - return object.Errorf("type error: arg '%v' of type '%v' is not supported", k, val.Type()) - } - } - } - } - - resp, err := um.sessionService.Query(ctx, expr, options) - - if err != nil { - return object.NewError(err) - } - return &resultSetProxy{resultSet: resp} -} - -func (um *sessionModule) resultSet(ctx context.Context, args ...object.Object) object.Object { - if err := require("session.result_set", 0, args); err != nil { - return err - } - - rs := um.sessionService.ResultSet(ctx) - if rs == nil { - return object.Nil - } - return &resultSetProxy{resultSet: rs} -} - -func (um *sessionModule) selectedItem(ctx context.Context, args ...object.Object) object.Object { - if err := require("session.result_set", 0, args); err != nil { - return err - } - - rs := um.sessionService.ResultSet(ctx) - idx := um.sessionService.SelectedItemIndex(ctx) - if rs == nil || idx < 0 { - return object.Nil - } - - rsProxy := &resultSetProxy{resultSet: rs} - return newItemProxy(rsProxy, idx) -} - -func (um *sessionModule) setResultSet(ctx context.Context, args ...object.Object) object.Object { - if err := require("session.set_result_set", 1, args); err != nil { - return err - } - - resultSetProxy, isResultSetProxy := args[0].(*resultSetProxy) - if !isResultSetProxy { - return object.NewError(errors.Errorf("type error: expected a resultsset (got %v)", args[0])) - } - - um.sessionService.SetResultSet(ctx, resultSetProxy.resultSet) - return nil -} - -func (um *sessionModule) currentTable(ctx context.Context, args ...object.Object) object.Object { - if err := require("session.current_table", 0, args); err != nil { - return err - } - - rs := um.sessionService.ResultSet(ctx) - if rs == nil { - return object.Nil - } - - return &tableProxy{table: rs.TableInfo} -} - -func (um *sessionModule) register() *object.Module { - return object.NewBuiltinsModule("session", map[string]object.Object{ - "query": object.NewBuiltin("query", um.query), - "current_table": object.NewBuiltin("current_table", um.currentTable), - "result_set": object.NewBuiltin("result_set", um.resultSet), - "selected_item": object.NewBuiltin("selected_item", um.selectedItem), - "set_result_set": object.NewBuiltin("set_result_set", um.setResultSet), - }) -} diff --git a/internal/dynamo-browse/services/scriptmanager/modsession_test.go b/internal/dynamo-browse/services/scriptmanager/modsession_test.go deleted file mode 100644 index 8712cf5..0000000 --- a/internal/dynamo-browse/services/scriptmanager/modsession_test.go +++ /dev/null @@ -1,426 +0,0 @@ -package scriptmanager_test - -import ( - "context" - "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" - "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" - "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/scriptmanager" - "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/scriptmanager/mocks" - "github.com/pkg/errors" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "testing" -) - -func TestModSession_Table(t *testing.T) { - t.Run("should return details of the current table", func(t *testing.T) { - tableDef := models.TableInfo{ - Name: "test_table", - Keys: models.KeyAttribute{ - PartitionKey: "pk", - SortKey: "sk", - }, - GSIs: []models.TableGSI{ - { - Name: "index-1", - Keys: models.KeyAttribute{ - PartitionKey: "ipk", - SortKey: "isk", - }, - }, - }, - } - rs := models.ResultSet{TableInfo: &tableDef} - - mockedSessionService := mocks.NewSessionService(t) - mockedSessionService.EXPECT().ResultSet(mock.Anything).Return(&rs) - - testFS := testScriptFile(t, "test.tm", ` - table := session.current_table() - - assert(table.name == "test_table") - 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["hash"] == "ipk") - assert(table.gsis[0].keys["range"] == "isk") - - assert(table == session.result_set().table) - `) - - srv := scriptmanager.New(scriptmanager.WithFS(testFS)) - srv.SetIFaces(scriptmanager.Ifaces{ - Session: mockedSessionService, - }) - - ctx := context.Background() - err := <-srv.RunAdHocScript(ctx, "test.tm") - assert.NoError(t, err) - - mockedSessionService.AssertExpectations(t) - }) - - t.Run("should return nil if no current result set", func(t *testing.T) { - mockedSessionService := mocks.NewSessionService(t) - mockedSessionService.EXPECT().ResultSet(mock.Anything).Return(nil) - - testFS := testScriptFile(t, "test.tm", ` - table := session.current_table() - - assert(table == nil) - `) - - srv := scriptmanager.New(scriptmanager.WithFS(testFS)) - srv.SetIFaces(scriptmanager.Ifaces{ - Session: mockedSessionService, - }) - - ctx := context.Background() - err := <-srv.RunAdHocScript(ctx, "test.tm") - assert.NoError(t, err) - - mockedSessionService.AssertExpectations(t) - }) -} - -func TestModSession_Query(t *testing.T) { - t.Run("should successfully return query result", func(t *testing.T) { - rs := &models.ResultSet{} - rs.SetItems([]models.Item{ - {"pk": &types.AttributeValueMemberS{Value: "abc"}}, - {"pk": &types.AttributeValueMemberS{Value: "1232"}}, - }) - - mockedSessionService := mocks.NewSessionService(t) - mockedSessionService.EXPECT().Query(mock.Anything, "some expr", scriptmanager.QueryOptions{}).Return(rs, nil) - - mockedUIService := mocks.NewUIService(t) - mockedUIService.EXPECT().PrintMessage(mock.Anything, "2") - mockedUIService.EXPECT().PrintMessage(mock.Anything, "res[0]['pk'].S = abc") - mockedUIService.EXPECT().PrintMessage(mock.Anything, "res[1]['pk'].S = 1232") - mockedUIService.EXPECT().PrintMessage(mock.Anything, "res[1].attr('size(pk)') = 4") - - testFS := testScriptFile(t, "test.tm", ` - res := session.query("some expr") - ui.print(res.length) - ui.print("res[0]['pk'].S = ", res[0].attr("pk")) - ui.print("res[1]['pk'].S = ", res[1].attr("pk")) - ui.print("res[1].attr('size(pk)') = ", res[1].attr("size(pk)")) - `) - - srv := scriptmanager.New(scriptmanager.WithFS(testFS)) - srv.SetIFaces(scriptmanager.Ifaces{ - UI: mockedUIService, - Session: mockedSessionService, - }) - - ctx := context.Background() - err := <-srv.RunAdHocScript(ctx, "test.tm") - assert.NoError(t, err) - - mockedUIService.AssertExpectations(t) - mockedSessionService.AssertExpectations(t) - }) - - t.Run("should return error if query returns error", func(t *testing.T) { - mockedSessionService := mocks.NewSessionService(t) - mockedSessionService.EXPECT().Query(mock.Anything, "some expr", scriptmanager.QueryOptions{}).Return(nil, errors.New("bang")) - - mockedUIService := mocks.NewUIService(t) - - testFS := testScriptFile(t, "test.tm", ` - res := session.query("some expr") - `) - - srv := scriptmanager.New(scriptmanager.WithFS(testFS)) - srv.SetIFaces(scriptmanager.Ifaces{ - UI: mockedUIService, - Session: mockedSessionService, - }) - - ctx := context.Background() - err := <-srv.RunAdHocScript(ctx, "test.tm") - assert.Error(t, err) - - mockedUIService.AssertExpectations(t) - mockedSessionService.AssertExpectations(t) - }) - - t.Run("should successfully specify table name", func(t *testing.T) { - rs := &models.ResultSet{} - - mockedSessionService := mocks.NewSessionService(t) - mockedSessionService.EXPECT().Query(mock.Anything, "some expr", scriptmanager.QueryOptions{ - TableName: "some-table", - }).Return(rs, nil) - - mockedUIService := mocks.NewUIService(t) - - testFS := testScriptFile(t, "test.tm", ` - res := session.query("some expr", { - table: "some-table", - }) - assert(res) - `) - - srv := scriptmanager.New(scriptmanager.WithFS(testFS)) - srv.SetIFaces(scriptmanager.Ifaces{ - UI: mockedUIService, - Session: mockedSessionService, - }) - - ctx := context.Background() - err := <-srv.RunAdHocScript(ctx, "test.tm") - assert.NoError(t, err) - - mockedUIService.AssertExpectations(t) - mockedSessionService.AssertExpectations(t) - }) - - t.Run("should successfully specify table proxy", func(t *testing.T) { - rs := &models.ResultSet{} - - mockedSessionService := mocks.NewSessionService(t) - mockedSessionService.EXPECT().ResultSet(mock.Anything).Return(&models.ResultSet{ - TableInfo: &models.TableInfo{ - Name: "some-resultset-table", - }, - }) - mockedSessionService.EXPECT().Query(mock.Anything, "some expr", scriptmanager.QueryOptions{ - TableName: "some-resultset-table", - }).Return(rs, nil) - - mockedUIService := mocks.NewUIService(t) - - testFS := testScriptFile(t, "test.tm", ` - res := session.query("some expr", { - table: session.result_set().table, - }) - assert(res) - `) - - srv := scriptmanager.New(scriptmanager.WithFS(testFS)) - srv.SetIFaces(scriptmanager.Ifaces{ - UI: mockedUIService, - Session: mockedSessionService, - }) - - ctx := context.Background() - err := <-srv.RunAdHocScript(ctx, "test.tm") - assert.NoError(t, err) - - mockedUIService.AssertExpectations(t) - mockedSessionService.AssertExpectations(t) - }) - - t.Run("should set placeholder values", func(t *testing.T) { - rs := &models.ResultSet{} - - mockedSessionService := mocks.NewSessionService(t) - mockedSessionService.EXPECT().Query(mock.Anything, ":name = $value", scriptmanager.QueryOptions{ - NamePlaceholders: map[string]string{ - "name": "hello", - "value": "world", - }, - ValuePlaceholders: map[string]types.AttributeValue{ - "name": &types.AttributeValueMemberS{Value: "hello"}, - "value": &types.AttributeValueMemberS{Value: "world"}, - }, - }).Return(rs, nil) - - mockedUIService := mocks.NewUIService(t) - - testFS := testScriptFile(t, "test.tm", ` - res := session.query(":name = $value", { - args: { - name: "hello", - value: "world", - }, - }) - assert(res) - `) - - srv := scriptmanager.New(scriptmanager.WithFS(testFS)) - srv.SetIFaces(scriptmanager.Ifaces{ - UI: mockedUIService, - Session: mockedSessionService, - }) - - ctx := context.Background() - err := <-srv.RunAdHocScript(ctx, "test.tm") - assert.NoError(t, err) - - mockedUIService.AssertExpectations(t) - mockedSessionService.AssertExpectations(t) - }) - - t.Run("should support various placeholder value type", func(t *testing.T) { - rs := &models.ResultSet{} - - mockedSessionService := mocks.NewSessionService(t) - mockedSessionService.EXPECT().Query(mock.Anything, ":name = $value", scriptmanager.QueryOptions{ - NamePlaceholders: map[string]string{ - "str": "hello", - }, - ValuePlaceholders: map[string]types.AttributeValue{ - "str": &types.AttributeValueMemberS{Value: "hello"}, - "int": &types.AttributeValueMemberN{Value: "123"}, - "float": &types.AttributeValueMemberN{Value: "3.14"}, - "bool": &types.AttributeValueMemberBOOL{Value: true}, - "nil": &types.AttributeValueMemberNULL{Value: true}, - }, - }).Return(rs, nil) - - mockedUIService := mocks.NewUIService(t) - - testFS := testScriptFile(t, "test.tm", ` - res := session.query(":name = $value", { - args: { - "str": "hello", - "int": 123, - "float": 3.14, - "bool": true, - "nil": nil, - }, - }) - assert(res) - `) - - srv := scriptmanager.New(scriptmanager.WithFS(testFS)) - srv.SetIFaces(scriptmanager.Ifaces{ - UI: mockedUIService, - Session: mockedSessionService, - }) - - ctx := context.Background() - err := <-srv.RunAdHocScript(ctx, "test.tm") - assert.NoError(t, err) - - mockedUIService.AssertExpectations(t) - mockedSessionService.AssertExpectations(t) - }) - - t.Run("should return error when placeholder value type is unsupported", func(t *testing.T) { - mockedSessionService := mocks.NewSessionService(t) - mockedUIService := mocks.NewUIService(t) - - testFS := testScriptFile(t, "test.tm", ` - res := session.query(":name = $value", { - args: { - "bad": func() { }, - }, - }) - `) - - srv := scriptmanager.New(scriptmanager.WithFS(testFS)) - srv.SetIFaces(scriptmanager.Ifaces{ - UI: mockedUIService, - Session: mockedSessionService, - }) - - ctx := context.Background() - err := <-srv.RunAdHocScript(ctx, "test.tm") - assert.Error(t, err) - - mockedUIService.AssertExpectations(t) - mockedSessionService.AssertExpectations(t) - }) -} - -func TestModSession_SelectedItem(t *testing.T) { - t.Run("should return selected item from service implementation", func(t *testing.T) { - rs := &models.ResultSet{} - rs.SetItems([]models.Item{ - {"pk": &types.AttributeValueMemberS{Value: "abc"}}, - {"pk": &types.AttributeValueMemberS{Value: "1232"}}, - }) - - mockedSessionService := mocks.NewSessionService(t) - mockedSessionService.EXPECT().ResultSet(mock.Anything).Return(rs) - mockedSessionService.EXPECT().SelectedItemIndex(mock.Anything).Return(1) - - testFS := testScriptFile(t, "test.tm", ` - selItem := session.selected_item() - - assert(selItem != nil, "selItem != nil") - assert(selItem.index == 1, "selItem.index") - assert(selItem.result_set == session.result_set(), "selItem.result_set") - assert(selItem.attr('pk') == '1232', "selItem.attr('pk')") - `) - - srv := scriptmanager.New(scriptmanager.WithFS(testFS)) - srv.SetIFaces(scriptmanager.Ifaces{ - Session: mockedSessionService, - }) - - ctx := context.Background() - err := <-srv.RunAdHocScript(ctx, "test.tm") - assert.NoError(t, err) - - mockedSessionService.AssertExpectations(t) - }) - - t.Run("should return nil if selected item returns -1", func(t *testing.T) { - rs := &models.ResultSet{} - rs.SetItems([]models.Item{ - {"pk": &types.AttributeValueMemberS{Value: "abc"}}, - {"pk": &types.AttributeValueMemberS{Value: "1232"}}, - }) - - mockedSessionService := mocks.NewSessionService(t) - mockedSessionService.EXPECT().ResultSet(mock.Anything).Return(rs) - mockedSessionService.EXPECT().SelectedItemIndex(mock.Anything).Return(-1) - - testFS := testScriptFile(t, "test.tm", ` - selItem := session.selected_item() - - assert(selItem == nil, "selItem != nil") - `) - - srv := scriptmanager.New(scriptmanager.WithFS(testFS)) - srv.SetIFaces(scriptmanager.Ifaces{ - Session: mockedSessionService, - }) - - ctx := context.Background() - err := <-srv.RunAdHocScript(ctx, "test.tm") - assert.NoError(t, err) - - mockedSessionService.AssertExpectations(t) - }) -} - -func TestModSession_SetResultSet(t *testing.T) { - t.Run("should set the result set on the session", func(t *testing.T) { - rs := &models.ResultSet{} - rs.SetItems([]models.Item{ - {"pk": &types.AttributeValueMemberS{Value: "abc"}}, - {"pk": &types.AttributeValueMemberS{Value: "1232"}}, - }) - - mockedSessionService := mocks.NewSessionService(t) - mockedSessionService.EXPECT().Query(mock.Anything, "some expr", scriptmanager.QueryOptions{}).Return(rs, nil) - mockedSessionService.EXPECT().SetResultSet(mock.Anything, rs) - - mockedUIService := mocks.NewUIService(t) - - testFS := testScriptFile(t, "test.tm", ` - res := session.query("some expr") - session.set_result_set(res) - `) - - srv := scriptmanager.New(scriptmanager.WithFS(testFS)) - srv.SetIFaces(scriptmanager.Ifaces{ - UI: mockedUIService, - Session: mockedSessionService, - }) - - ctx := context.Background() - err := <-srv.RunAdHocScript(ctx, "test.tm") - assert.NoError(t, err) - - mockedUIService.AssertExpectations(t) - mockedSessionService.AssertExpectations(t) - }) -} diff --git a/internal/dynamo-browse/services/scriptmanager/modui.go b/internal/dynamo-browse/services/scriptmanager/modui.go deleted file mode 100644 index d53b2e4..0000000 --- a/internal/dynamo-browse/services/scriptmanager/modui.go +++ /dev/null @@ -1,58 +0,0 @@ -package scriptmanager - -import ( - "context" - "strings" - - "github.com/risor-io/risor/object" -) - -type uiModule struct { - uiService UIService -} - -func (um *uiModule) print(ctx context.Context, args ...object.Object) object.Object { - var msg strings.Builder - for _, arg := range args { - if arg == nil { - continue - } - - switch a := arg.(type) { - case *object.String: - msg.WriteString(a.Value()) - default: - msg.WriteString(a.Inspect()) - } - } - - um.uiService.PrintMessage(ctx, msg.String()) - return object.Nil -} - -func (um *uiModule) prompt(ctx context.Context, args ...object.Object) object.Object { - if err := require("ui.prompt", 1, args); err != nil { - return err - } - - msg, _ := object.AsString(args[0]) - respChan := um.uiService.Prompt(ctx, msg) - - select { - case resp, hasResp := <-respChan: - if hasResp { - return object.NewString(resp) - } else { - return object.Nil - } - case <-ctx.Done(): - return object.NewError(ctx.Err()) - } -} - -func (um *uiModule) register() *object.Module { - return object.NewBuiltinsModule("ui", map[string]object.Object{ - "print": object.NewBuiltin("print", um.print), - "prompt": object.NewBuiltin("prompt", um.prompt), - }) -} diff --git a/internal/dynamo-browse/services/scriptmanager/modui_test.go b/internal/dynamo-browse/services/scriptmanager/modui_test.go deleted file mode 100644 index 3a8b96d..0000000 --- a/internal/dynamo-browse/services/scriptmanager/modui_test.go +++ /dev/null @@ -1,100 +0,0 @@ -package scriptmanager_test - -import ( - "context" - "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/scriptmanager" - "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/scriptmanager/mocks" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "testing" -) - -func TestModUI_Prompt(t *testing.T) { - t.Run("should successfully return prompt value", func(t *testing.T) { - testFS := testScriptFile(t, "test.tm", ` - ui.print("Hello, world") - var name = ui.prompt("What is your name? ") - ui.print("Hello, " + name) - `) - - promptChan := make(chan string) - go func() { - promptChan <- "T. Test" - }() - - mockedUIService := mocks.NewUIService(t) - mockedUIService.EXPECT().PrintMessage(mock.Anything, "Hello, world") - mockedUIService.EXPECT().Prompt(mock.Anything, "What is your name? ").Return(promptChan) - mockedUIService.EXPECT().PrintMessage(mock.Anything, "Hello, T. Test") - - srv := scriptmanager.New(scriptmanager.WithFS(testFS)) - srv.SetIFaces(scriptmanager.Ifaces{ - UI: mockedUIService, - }) - - ctx := context.Background() - err := <-srv.RunAdHocScript(ctx, "test.tm") - assert.NoError(t, err) - - mockedUIService.AssertExpectations(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) - close(promptChan) - - ctx := context.Background() - - 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)) - srv.SetIFaces(scriptmanager.Ifaces{ - UI: mockedUIService, - }) - - err := <-srv.RunAdHocScript(ctx, "test.tm") - assert.NoError(t, err) - - mockedUIService.AssertExpectations(t) - }) - - t.Run("should return error if context 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") - `) - - promptChan := make(chan string) - ctx, cancelFn := context.WithCancel(context.Background()) - defer cancelFn() - - mockedUIService := mocks.NewUIService(t) - mockedUIService.EXPECT().PrintMessage(mock.Anything, "Hello, world") - mockedUIService.EXPECT().Prompt(mock.Anything, "What is your name? ").Run(func(ctx context.Context, msg string) { - cancelFn() - }).Return(promptChan) - - srv := scriptmanager.New(scriptmanager.WithFS(testFS)) - srv.SetIFaces(scriptmanager.Ifaces{ - UI: mockedUIService, - }) - - err := <-srv.RunAdHocScript(ctx, "test.tm") - assert.Error(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 deleted file mode 100644 index 39d961f..0000000 --- a/internal/dynamo-browse/services/scriptmanager/opts.go +++ /dev/null @@ -1,26 +0,0 @@ -package scriptmanager - -import ( - "context" - "github.com/risor-io/risor/limits" -) - -// scriptEnv is the runtime environment for a particular script execution -type scriptEnv struct { - filename string -} - -type scriptEnvKeyType struct{} - -var scriptEnvKey = scriptEnvKeyType{} - -func scriptEnvFromCtx(ctx context.Context) scriptEnv { - perms, _ := ctx.Value(scriptEnvKey).(scriptEnv) - return perms -} - -func ctxWithScriptEnv(ctx context.Context, perms scriptEnv) context.Context { - newCtx := context.WithValue(ctx, scriptEnvKey, perms) - newCtx = limits.WithLimits(newCtx, limits.New()) - return newCtx -} diff --git a/internal/dynamo-browse/services/scriptmanager/relitem.go b/internal/dynamo-browse/services/scriptmanager/relitem.go deleted file mode 100644 index 63d7629..0000000 --- a/internal/dynamo-browse/services/scriptmanager/relitem.go +++ /dev/null @@ -1,57 +0,0 @@ -package scriptmanager - -import ( - "context" - "log" - "path" - - "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" - "github.com/lmika/dynamo-browse/internal/dynamo-browse/models/queryexpr" - "github.com/lmika/dynamo-browse/internal/dynamo-browse/models/relitems" -) - -type relatedItem struct { - label string - table string - query *queryexpr.QueryExpr - onSelect func() error -} - -type relatedItemBuilder struct { - table string - itemProduction func(ctx context.Context, rs *models.ResultSet, index int) ([]relatedItem, error) -} - -func (s *Service) RelatedItemOfItem(ctx context.Context, rs *models.ResultSet, index int) ([]relitems.RelatedItem, error) { - riModels := []relitems.RelatedItem{} - - for _, plugin := range s.plugins { - for _, rb := range plugin.relatedItems { - // TODO: should support matching - match, _ := tableMatchesGlob(rb.table, rs.TableInfo.Name) - log.Printf("RelatedItemOfItem: table = '%v', pattern = '%v', match = '%v'", rb.table, rs.TableInfo.Name, match) - if match { - relatedItems, err := rb.itemProduction(ctx, rs, index) - if err != nil { - // TODO: should probably return error if no rel items were found and an error was raised - return nil, err - } - - // TODO: make this nicer - for _, ri := range relatedItems { - riModels = append(riModels, relitems.RelatedItem{ - Name: ri.label, - Query: ri.query, - Table: ri.table, - OnSelect: ri.onSelect, - }) - } - } - } - } - return riModels, nil -} - -func tableMatchesGlob(tableName, pattern string) (bool, error) { - return path.Match(tableName, pattern) -} diff --git a/internal/dynamo-browse/services/scriptmanager/resultsetproxy.go b/internal/dynamo-browse/services/scriptmanager/resultsetproxy.go deleted file mode 100644 index 953a3e4..0000000 --- a/internal/dynamo-browse/services/scriptmanager/resultsetproxy.go +++ /dev/null @@ -1,337 +0,0 @@ -package scriptmanager - -import ( - "context" - "time" - - "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" - "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" - "github.com/lmika/dynamo-browse/internal/dynamo-browse/models/attrutils" - "github.com/lmika/dynamo-browse/internal/dynamo-browse/models/queryexpr" - "github.com/pkg/errors" - "github.com/risor-io/risor/object" - "github.com/risor-io/risor/op" -) - -type resultSetProxy struct { - resultSet *models.ResultSet -} - -func newResultSetProxy(rs *models.ResultSet) *resultSetProxy { - return &resultSetProxy{resultSet: rs} -} - -func (r *resultSetProxy) SetAttr(name string, value object.Object) error { - return errors.Errorf("attribute error: %v", name) -} - -func (r *resultSetProxy) RunOperation(opType op.BinaryOpType, right object.Object) object.Object { - return object.Errorf("op error: unsupported %v", opType) -} - -func (r *resultSetProxy) Cost() int { - return len(r.resultSet.Items()) -} - -func (r *resultSetProxy) Interface() interface{} { - return r.resultSet -} - -func (r *resultSetProxy) IsTruthy() bool { - return true -} - -func (r *resultSetProxy) Type() object.Type { - return "resultset" -} - -func (r *resultSetProxy) Inspect() string { - return "resultset" -} - -func (r *resultSetProxy) Equals(other object.Object) object.Object { - otherRS, isOtherRS := other.(*resultSetProxy) - if !isOtherRS { - return object.False - } - - return object.NewBool(r.resultSet == otherRS.resultSet) -} - -// GetItem implements the [key] operator for a container type. -func (r *resultSetProxy) GetItem(key object.Object) (object.Object, *object.Error) { - idx, err := object.AsInt(key) - if err != nil { - return nil, err - } - - realIdx := int(idx) - if realIdx < 0 { - realIdx = len(r.resultSet.Items()) + realIdx - } - - if realIdx < 0 || realIdx >= len(r.resultSet.Items()) { - return nil, object.NewError(errors.Errorf("index error: index out of range: %v", idx)) - } - - return newItemProxy(r, realIdx), nil -} - -// GetSlice implements the [start:stop] operator for a container type. -func (r *resultSetProxy) GetSlice(s object.Slice) (object.Object, *object.Error) { - return nil, object.NewError(errors.New("TODO")) -} - -// SetItem implements the [key] = value operator for a container type. -func (r *resultSetProxy) SetItem(key, value object.Object) *object.Error { - return object.NewError(errors.New("TODO")) -} - -// DelItem implements the del [key] operator for a container type. -func (r *resultSetProxy) DelItem(key object.Object) *object.Error { - return object.NewError(errors.New("TODO")) -} - -// Contains returns true if the given item is found in this container. -func (r *resultSetProxy) Contains(item object.Object) *object.Bool { - // TODO - return object.False -} - -// Len returns the number of items in this container. -func (r *resultSetProxy) Len() *object.Int { - return object.NewInt(int64(len(r.resultSet.Items()))) -} - -// Iter returns an iterator for this container. -func (r *resultSetProxy) Iter() object.Iterator { - // TODO - return nil -} - -func (r *resultSetProxy) GetAttr(name string) (object.Object, bool) { - switch name { - case "table": - return &tableProxy{table: r.resultSet.TableInfo}, true - case "length": - return object.NewInt(int64(len(r.resultSet.Items()))), true - case "find": - return object.NewBuiltin("find", r.find), true - case "merge": - return object.NewBuiltin("merge", r.merge), true - } - - return nil, false -} - -func (i *resultSetProxy) find(ctx context.Context, args ...object.Object) object.Object { - if objErr := require("resultset.find", 1, args); objErr != nil { - return objErr - } - - str, objErr := object.AsString(args[0]) - if objErr != nil { - return objErr - } - - modExpr, err := queryexpr.Parse(str) - if err != nil { - return object.Errorf("arg error: invalid path expression: %v", err) - } - - for idx, item := range i.resultSet.Items() { - rs, err := modExpr.EvalItem(item) - if err != nil { - continue - } - - if attrutils.Truthy(rs) { - return newItemProxy(i, idx) - } - } - - return object.Nil -} - -func (i *resultSetProxy) merge(ctx context.Context, args ...object.Object) object.Object { - type pksk struct { - pk types.AttributeValue - sk types.AttributeValue - } - - if objErr := require("resultset.merge", 1, args); objErr != nil { - return objErr - } - - otherRS, isRS := args[0].(*resultSetProxy) - if !isRS { - return object.NewError(errors.Errorf("type error: expected a resultset (got %v)", args[0].Type())) - } - - if !i.resultSet.TableInfo.Equal(otherRS.resultSet.TableInfo) { - return object.Nil - } - - itemsInI := make(map[pksk]models.Item) - newItems := make([]models.Item, 0, len(i.resultSet.Items())+len(otherRS.resultSet.Items())) - for _, item := range i.resultSet.Items() { - pk, sk := item.PKSK(i.resultSet.TableInfo) - itemsInI[pksk{pk, sk}] = item - newItems = append(newItems, item) - } - - for _, item := range otherRS.resultSet.Items() { - pk, sk := item.PKSK(i.resultSet.TableInfo) - if _, hasItem := itemsInI[pksk{pk, sk}]; !hasItem { - newItems = append(newItems, item) - } - } - - newResultSet := &models.ResultSet{ - Created: time.Now(), - TableInfo: i.resultSet.TableInfo, - } - newResultSet.SetItems(newItems) - - return &resultSetProxy{resultSet: newResultSet} -} - -type itemProxy struct { - resultSetProxy *resultSetProxy - itemIndex int - item models.Item -} - -func (i *itemProxy) SetAttr(name string, value object.Object) error { - return errors.Errorf("attribute error: %v", name) -} - -func (i *itemProxy) RunOperation(opType op.BinaryOpType, right object.Object) object.Object { - return object.Errorf("op error: unsupported %v", opType) -} - -func (i *itemProxy) Cost() int { - return len(i.item) -} - -func newItemProxy(rs *resultSetProxy, itemIndex int) *itemProxy { - return &itemProxy{ - resultSetProxy: rs, - itemIndex: itemIndex, - item: rs.resultSet.Items()[itemIndex], - } -} - -func (i *itemProxy) Interface() interface{} { - return i.item -} - -func (i *itemProxy) IsTruthy() bool { - return true -} - -func (i *itemProxy) Type() object.Type { - return "item" -} - -func (i *itemProxy) Inspect() string { - return "item" -} - -func (i *itemProxy) Equals(other object.Object) object.Object { - // TODO - return object.False -} - -func (i *itemProxy) GetAttr(name string) (object.Object, bool) { - // TODO: this should implement the container interface - switch name { - case "result_set": - return i.resultSetProxy, true - case "index": - return object.NewInt(int64(i.itemIndex)), true - case "attr": - return object.NewBuiltin("attr", i.value), true - case "set_attr": - return object.NewBuiltin("set_attr", i.setValue), true - case "delete_attr": - return object.NewBuiltin("delete_attr", i.deleteAttr), true - } - - return nil, false -} - -func (i *itemProxy) value(ctx context.Context, args ...object.Object) object.Object { - if objErr := require("item.attr", 1, args); objErr != nil { - return objErr - } - - str, objErr := object.AsString(args[0]) - if objErr != nil { - return objErr - } - - modExpr, err := queryexpr.Parse(str) - if err != nil { - return object.Errorf("arg error: invalid path expression: %v", err) - } - av, err := modExpr.EvalItem(i.item) - if err != nil { - return object.NewError(errors.Errorf("arg error: path expression evaluate error: %v", err)) - } - - tVal, err := attributeValueToTamarin(av) - if err != nil { - return object.NewError(err) - } - return tVal -} - -func (i *itemProxy) setValue(ctx context.Context, args ...object.Object) object.Object { - if objErr := require("item.set_attr", 2, args); objErr != nil { - return objErr - } - - pathExpr, objErr := object.AsString(args[0]) - if objErr != nil { - return objErr - } - - path, err := queryexpr.Parse(pathExpr) - if err != nil { - return object.Errorf("arg error: invalid path expression: %v", err) - } - - newValue, err := tamarinValueToAttributeValue(args[1]) - if err != nil { - return object.NewError(err) - } - if err := path.SetEvalItem(i.item, newValue); err != nil { - return object.NewError(err) - } - - i.resultSetProxy.resultSet.SetDirty(i.itemIndex, true) - return nil -} - -func (i *itemProxy) deleteAttr(ctx context.Context, args ...object.Object) object.Object { - if objErr := require("item.delete_attr", 1, args); objErr != nil { - return objErr - } - - str, objErr := object.AsString(args[0]) - if objErr != nil { - return objErr - } - - modExpr, err := queryexpr.Parse(str) - if err != nil { - return object.Errorf("arg error: invalid path expression: %v", err) - } - if err := modExpr.DeleteAttribute(i.item); err != nil { - return object.NewError(errors.Errorf("arg error: path expression evaluate error: %v", err)) - } - - i.resultSetProxy.resultSet.SetDirty(i.itemIndex, true) - return nil -} diff --git a/internal/dynamo-browse/services/scriptmanager/resultsetproxy_test.go b/internal/dynamo-browse/services/scriptmanager/resultsetproxy_test.go deleted file mode 100644 index 3b7f354..0000000 --- a/internal/dynamo-browse/services/scriptmanager/resultsetproxy_test.go +++ /dev/null @@ -1,355 +0,0 @@ -package scriptmanager_test - -import ( - "context" - "testing" - - "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" - "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" - "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/scriptmanager" - "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/scriptmanager/mocks" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" -) - -func TestResultSetProxy(t *testing.T) { - t.Run("should property return properties of a resultset and item", func(t *testing.T) { - rs := &models.ResultSet{ - TableInfo: &models.TableInfo{ - Name: "test-table", - }, - } - rs.SetItems([]models.Item{ - {"pk": &types.AttributeValueMemberS{Value: "abc"}}, - {"pk": &types.AttributeValueMemberS{Value: "1232"}}, - }) - - mockedSessionService := mocks.NewSessionService(t) - mockedSessionService.EXPECT().Query(mock.Anything, "some expr", scriptmanager.QueryOptions{}).Return(rs, nil) - - mockedUIService := mocks.NewUIService(t) - - testFS := testScriptFile(t, "test.tm", ` - res := session.query("some expr") - - // Test properties of the result set - assert(res.table.name, "hello") - - assert(res == res, "result_set.equals") - assert(res.length == 2, "result_set.length") - - // Test properties of items - assert(res[0].index == 0, "res[0].index") - assert(res[0].result_set == res, "res[0].result_set") - assert(res[0].attr('pk') == 'abc', "res[0].attr('pk')") - - assert(res[1].attr('pk') == '1232', "res[1].attr('pk')") - `) - - srv := scriptmanager.New(scriptmanager.WithFS(testFS)) - srv.SetIFaces(scriptmanager.Ifaces{ - UI: mockedUIService, - Session: mockedSessionService, - }) - - ctx := context.Background() - err := <-srv.RunAdHocScript(ctx, "test.tm") - assert.NoError(t, err) - - mockedUIService.AssertExpectations(t) - mockedSessionService.AssertExpectations(t) - }) -} - -func TestResultSetProxy_Find(t *testing.T) { - t.Run("should return the first item that matches the given expression", func(t *testing.T) { - rs := &models.ResultSet{} - rs.SetItems([]models.Item{ - {"pk": &types.AttributeValueMemberS{Value: "abc"}}, - {"pk": &types.AttributeValueMemberS{Value: "abc"}, "sk": &types.AttributeValueMemberS{Value: "abc"}, "primary": &types.AttributeValueMemberS{Value: "yes"}}, - {"pk": &types.AttributeValueMemberS{Value: "1232"}, "findMe": &types.AttributeValueMemberS{Value: "yes"}}, - {"pk": &types.AttributeValueMemberS{Value: "2345"}, "findMe": &types.AttributeValueMemberS{Value: "second"}}, - }) - - mockedSessionService := mocks.NewSessionService(t) - mockedSessionService.EXPECT().Query(mock.Anything, "some expr", scriptmanager.QueryOptions{}).Return(rs, nil) - - testFS := testScriptFile(t, "test.tm", ` - res := session.query("some expr") - - assert(res.find('findMe is "any"').attr("pk") == "1232") - assert(res.find('findMe = "second"').attr("pk") == "2345") - assert(res.find('pk = sk').attr("primary") == "yes") - - assert(res.find('findMe = "missing"') == nil) - `) - - srv := scriptmanager.New(scriptmanager.WithFS(testFS)) - srv.SetIFaces(scriptmanager.Ifaces{ - Session: mockedSessionService, - }) - - ctx := context.Background() - err := <-srv.RunAdHocScript(ctx, "test.tm") - assert.NoError(t, err) - - mockedSessionService.AssertExpectations(t) - }) -} - -func TestResultSetProxy_Merge(t *testing.T) { - t.Run("should return a result set with items from both if both are from the same table", func(t *testing.T) { - td := &models.TableInfo{Name: "test", Keys: models.KeyAttribute{PartitionKey: "pk", SortKey: "sk"}} - - rs1 := &models.ResultSet{TableInfo: td} - rs1.SetItems([]models.Item{ - {"pk": &types.AttributeValueMemberS{Value: "abc"}, "sk": &types.AttributeValueMemberS{Value: "123"}}, - }) - - rs2 := &models.ResultSet{TableInfo: td} - rs2.SetItems([]models.Item{ - {"pk": &types.AttributeValueMemberS{Value: "bcd"}, "sk": &types.AttributeValueMemberS{Value: "234"}}, - }) - - mockedSessionService := mocks.NewSessionService(t) - mockedSessionService.EXPECT().Query(mock.Anything, "rs1", scriptmanager.QueryOptions{}).Return(rs1, nil) - mockedSessionService.EXPECT().Query(mock.Anything, "rs2", scriptmanager.QueryOptions{}).Return(rs2, nil) - - testFS := testScriptFile(t, "test.tm", ` - r1 := session.query("rs1") - r2 := session.query("rs2") - - res := r1.merge(r2) - - assert(res[0].attr("pk") == "abc") - assert(res[0].attr("sk") == "123") - assert(res[1].attr("pk") == "bcd") - assert(res[1].attr("sk") == "234") - `) - - srv := scriptmanager.New(scriptmanager.WithFS(testFS)) - srv.SetIFaces(scriptmanager.Ifaces{ - Session: mockedSessionService, - }) - - ctx := context.Background() - err := <-srv.RunAdHocScript(ctx, "test.tm") - assert.NoError(t, err) - - mockedSessionService.AssertExpectations(t) - }) - - t.Run("should return nil if result-sets are from different tables", func(t *testing.T) { - td1 := &models.TableInfo{Name: "test", Keys: models.KeyAttribute{PartitionKey: "pk", SortKey: "sk"}} - rs1 := &models.ResultSet{TableInfo: td1} - rs1.SetItems([]models.Item{ - {"pk": &types.AttributeValueMemberS{Value: "abc"}, "sk": &types.AttributeValueMemberS{Value: "123"}}, - }) - - td2 := &models.TableInfo{Name: "test2", Keys: models.KeyAttribute{PartitionKey: "pk2", SortKey: "sk"}} - rs2 := &models.ResultSet{TableInfo: td2} - rs2.SetItems([]models.Item{ - {"pk": &types.AttributeValueMemberS{Value: "bcd"}, "sk": &types.AttributeValueMemberS{Value: "234"}}, - }) - - mockedSessionService := mocks.NewSessionService(t) - mockedSessionService.EXPECT().Query(mock.Anything, "rs1", scriptmanager.QueryOptions{}).Return(rs1, nil) - mockedSessionService.EXPECT().Query(mock.Anything, "rs2", scriptmanager.QueryOptions{}).Return(rs2, nil) - - testFS := testScriptFile(t, "test.tm", ` - r1 := session.query("rs1") - r2 := session.query("rs2") - - res := r1.merge(r2) - - assert(res == nil) - `) - - srv := scriptmanager.New(scriptmanager.WithFS(testFS)) - srv.SetIFaces(scriptmanager.Ifaces{ - Session: mockedSessionService, - }) - - ctx := context.Background() - err := <-srv.RunAdHocScript(ctx, "test.tm") - assert.NoError(t, err) - - mockedSessionService.AssertExpectations(t) - }) -} - -func TestResultSetProxy_GetAttr(t *testing.T) { - t.Run("should return the value of items within a result set", func(t *testing.T) { - rs := &models.ResultSet{} - rs.SetItems([]models.Item{ - { - "pk": &types.AttributeValueMemberS{Value: "abc"}, - "sk": &types.AttributeValueMemberN{Value: "123"}, - "bool": &types.AttributeValueMemberBOOL{Value: true}, - "null": &types.AttributeValueMemberNULL{Value: true}, - "list": &types.AttributeValueMemberL{Value: []types.AttributeValue{ - &types.AttributeValueMemberS{Value: "apple"}, - &types.AttributeValueMemberS{Value: "banana"}, - &types.AttributeValueMemberS{Value: "cherry"}, - }}, - "map": &types.AttributeValueMemberM{Value: map[string]types.AttributeValue{ - "this": &types.AttributeValueMemberS{Value: "that"}, - "another": &types.AttributeValueMemberS{Value: "thing"}, - }}, - "strSet": &types.AttributeValueMemberSS{Value: []string{"apple", "banana", "cherry"}}, - "numSet": &types.AttributeValueMemberNS{Value: []string{"123", "45.67", "8.911", "-321"}}, - }, - }) - - mockedSessionService := mocks.NewSessionService(t) - mockedSessionService.EXPECT().Query(mock.Anything, "some expr", scriptmanager.QueryOptions{}).Return(rs, nil) - - testFS := testScriptFile(t, "test.tm", ` - res := session.query("some expr") - - assert(res[0].attr("pk") == "abc", "str attr") - assert(res[0].attr("sk") == 123, "num attr") - assert(res[0].attr("bool") == true, "bool attr") - assert(res[0].attr("null") == nil, "null attr") - assert(res[0].attr("list") == ["apple","banana","cherry"], "list attr") - assert(res[0].attr("map") == {"this":"that", "another":"thing"}, "map attr") - assert(res[0].attr("strSet") == {"apple","banana","cherry"}, "string set") - assert(res[0].attr("numSet") == {123, 45.67, 8.911, -321}, "number set") - `) - - srv := scriptmanager.New(scriptmanager.WithFS(testFS)) - srv.SetIFaces(scriptmanager.Ifaces{ - Session: mockedSessionService, - }) - - ctx := context.Background() - err := <-srv.RunAdHocScript(ctx, "test.tm") - assert.NoError(t, err) - - mockedSessionService.AssertExpectations(t) - }) -} - -func TestResultSetProxy_SetAttr(t *testing.T) { - t.Run("should set the value of the item within a result set", func(t *testing.T) { - rs := &models.ResultSet{} - rs.SetItems([]models.Item{ - {"pk": &types.AttributeValueMemberS{Value: "abc"}}, - {"pk": &types.AttributeValueMemberS{Value: "1232"}}, - }) - - mockedSessionService := mocks.NewSessionService(t) - mockedSessionService.EXPECT().Query(mock.Anything, "some expr", scriptmanager.QueryOptions{}).Return(rs, nil) - mockedSessionService.EXPECT().SetResultSet(mock.Anything, mock.MatchedBy(func(rs *models.ResultSet) bool { - assert.Equal(t, "bla-di-bla", rs.Items()[0]["pk"].(*types.AttributeValueMemberS).Value) - assert.Equal(t, "123", rs.Items()[0]["num"].(*types.AttributeValueMemberN).Value) - assert.Equal(t, "123.45", rs.Items()[0]["numFloat"].(*types.AttributeValueMemberN).Value) - assert.Equal(t, true, rs.Items()[0]["bool"].(*types.AttributeValueMemberBOOL).Value) - assert.Equal(t, true, rs.Items()[0]["nil"].(*types.AttributeValueMemberNULL).Value) - - list := rs.Items()[0]["lists"].(*types.AttributeValueMemberL).Value - assert.Equal(t, "abc", list[0].(*types.AttributeValueMemberS).Value) - assert.Equal(t, "123", list[1].(*types.AttributeValueMemberN).Value) - assert.Equal(t, true, list[2].(*types.AttributeValueMemberBOOL).Value) - - nestedLists := rs.Items()[0]["nestedLists"].(*types.AttributeValueMemberL).Value - assert.Equal(t, "1", nestedLists[0].(*types.AttributeValueMemberL).Value[0].(*types.AttributeValueMemberN).Value) - assert.Equal(t, "2", nestedLists[0].(*types.AttributeValueMemberL).Value[1].(*types.AttributeValueMemberN).Value) - assert.Equal(t, "3", nestedLists[1].(*types.AttributeValueMemberL).Value[0].(*types.AttributeValueMemberN).Value) - assert.Equal(t, "4", nestedLists[1].(*types.AttributeValueMemberL).Value[1].(*types.AttributeValueMemberN).Value) - - mapValue := rs.Items()[0]["map"].(*types.AttributeValueMemberM).Value - assert.Equal(t, "world", mapValue["hello"].(*types.AttributeValueMemberS).Value) - assert.Equal(t, "213", mapValue["nums"].(*types.AttributeValueMemberN).Value) - - numSet := rs.Items()[0]["numSet"].(*types.AttributeValueMemberNS).Value - assert.Len(t, numSet, 4) - assert.Contains(t, numSet, "1") - assert.Contains(t, numSet, "2") - assert.Contains(t, numSet, "3") - assert.Contains(t, numSet, "4.5") - - strSet := rs.Items()[0]["strSet"].(*types.AttributeValueMemberSS).Value - assert.Len(t, strSet, 3) - assert.Contains(t, strSet, "a") - assert.Contains(t, strSet, "b") - assert.Contains(t, strSet, "c") - - assert.True(t, rs.IsDirty(0)) - return true - })) - - mockedUIService := mocks.NewUIService(t) - - testFS := testScriptFile(t, "test.tm", ` - res := session.query("some expr") - - res[0].set_attr("pk", "bla-di-bla") - res[0].set_attr("num", 123) - res[0].set_attr("numFloat", 123.45) - res[0].set_attr("bool", true) - res[0].set_attr("nil", nil) - res[0].set_attr("lists", ['abc', 123, true]) - res[0].set_attr("nestedLists", [[1,2], [3,4]]) - res[0].set_attr("map", {"hello": "world", "nums": 213}) - res[0].set_attr("numSet", {1,2,3,4.5}) - res[0].set_attr("strSet", {"a","b","c"}) - - session.set_result_set(res) - `) - - srv := scriptmanager.New(scriptmanager.WithFS(testFS)) - srv.SetIFaces(scriptmanager.Ifaces{ - UI: mockedUIService, - Session: mockedSessionService, - }) - - ctx := context.Background() - err := <-srv.RunAdHocScript(ctx, "test.tm") - assert.NoError(t, err) - - mockedUIService.AssertExpectations(t) - mockedSessionService.AssertExpectations(t) - }) -} - -func TestResultSetProxy_DeleteAttr(t *testing.T) { - t.Run("should delete the value of the item within a result set", func(t *testing.T) { - rs := &models.ResultSet{} - rs.SetItems([]models.Item{ - {"pk": &types.AttributeValueMemberS{Value: "abc"}, "deleteMe": &types.AttributeValueMemberBOOL{Value: true}}, - {"pk": &types.AttributeValueMemberS{Value: "1232"}}, - }) - - mockedSessionService := mocks.NewSessionService(t) - mockedSessionService.EXPECT().Query(mock.Anything, "some expr", scriptmanager.QueryOptions{}).Return(rs, nil) - mockedSessionService.EXPECT().SetResultSet(mock.Anything, mock.MatchedBy(func(rs *models.ResultSet) bool { - assert.Equal(t, "abc", rs.Items()[0]["pk"].(*types.AttributeValueMemberS).Value) - assert.Nil(t, rs.Items()[0]["deleteMe"]) - assert.True(t, rs.IsDirty(0)) - return true - })) - - mockedUIService := mocks.NewUIService(t) - - testFS := testScriptFile(t, "test.tm", ` - res := session.query("some expr") - res[0].delete_attr("deleteMe") - session.set_result_set(res) - `) - - srv := scriptmanager.New(scriptmanager.WithFS(testFS)) - srv.SetIFaces(scriptmanager.Ifaces{ - UI: mockedUIService, - Session: mockedSessionService, - }) - - ctx := context.Background() - err := <-srv.RunAdHocScript(ctx, "test.tm") - assert.NoError(t, err) - - mockedUIService.AssertExpectations(t) - mockedSessionService.AssertExpectations(t) - }) - -} diff --git a/internal/dynamo-browse/services/scriptmanager/scrsched.go b/internal/dynamo-browse/services/scriptmanager/scrsched.go deleted file mode 100644 index 3846676..0000000 --- a/internal/dynamo-browse/services/scriptmanager/scrsched.go +++ /dev/null @@ -1,53 +0,0 @@ -package scriptmanager - -import ( - "context" - "github.com/pkg/errors" - "time" -) - -type scriptScheduler struct { - jobChan chan scriptJob -} - -func newScriptScheduler() *scriptScheduler { - ss := &scriptScheduler{} - ss.start() - return ss -} - -func (ss *scriptScheduler) start() { - ss.jobChan = make(chan scriptJob) - go func() { - for job := range ss.jobChan { - job.job(job.ctx) - } - }() -} - -// startJobOnceFree will submit a script execution job. The function will wait until the scheduler is free. -// The job will then run on the script goroutine and the function will return. -func (ss *scriptScheduler) startJobOnceFree(ctx context.Context, job func(ctx context.Context)) error { - select { - case ss.jobChan <- scriptJob{ctx: ctx, job: job}: - return nil - case <-ctx.Done(): - return ctx.Err() - } -} - -// runNow will submit a job for immediate execution. The job will run as long as the scheduler is free. -// If the scheduler is not free, an error will be returned and the job will not run. -func (ss *scriptScheduler) runNow(ctx context.Context, job func(ctx context.Context)) error { - select { - case ss.jobChan <- scriptJob{ctx: ctx, job: job}: - return nil - case <-time.After(500 * time.Millisecond): - return errors.New("a script is already running") - } -} - -type scriptJob struct { - ctx context.Context - job func(ctx context.Context) -} diff --git a/internal/dynamo-browse/services/scriptmanager/service.go b/internal/dynamo-browse/services/scriptmanager/service.go deleted file mode 100644 index eac6638..0000000 --- a/internal/dynamo-browse/services/scriptmanager/service.go +++ /dev/null @@ -1,250 +0,0 @@ -package scriptmanager - -import ( - "context" - "io/fs" - "log" - "os" - "path/filepath" - "strings" - - "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/keybindings" - "github.com/pkg/errors" - "github.com/risor-io/risor" - "github.com/risor-io/risor/object" -) - -var ( - relPrefix = "." + string(filepath.Separator) -) - -type Service struct { - lookupPaths []fs.FS - ifaces Ifaces - sched *scriptScheduler - plugins []*ScriptPlugin -} - -func New(opts ...ServiceOption) *Service { - srv := &Service{ - lookupPaths: nil, - sched: newScriptScheduler(), - } - for _, opt := range opts { - opt(srv) - } - return srv -} - -func (s *Service) SetLookupPaths(fs []fs.FS) { - s.lookupPaths = fs -} - -func (s *Service) SetIFaces(ifaces Ifaces) { - s.ifaces = ifaces -} - -func (s *Service) LoadScript(ctx context.Context, filename string) (*ScriptPlugin, error) { - resChan := make(chan loadedScriptResult) - - if err := s.sched.startJobOnceFree(ctx, func(ctx context.Context) { - s.loadScript(ctx, filename, resChan) - }); err != nil { - return nil, err - } - - res := <-resChan - if res.err != nil { - return nil, res.err - } - - // Look for the previous version. If one is there, replace it, otherwise add it - // TODO: this should probably be protected by a mutex - newPlugin := res.scriptPlugin - for i, p := range s.plugins { - if p.name == newPlugin.name { - s.plugins[i] = newPlugin - return newPlugin, nil - } - } - - s.plugins = append(s.plugins, newPlugin) - return newPlugin, nil -} - -func (s *Service) RunAdHocScript(ctx context.Context, filename string) chan error { - errChan := make(chan error) - go s.startAdHocScript(ctx, filename, errChan) - return errChan -} - -func (s *Service) StartAdHocScript(ctx context.Context, filename string, errChan chan error) error { - return s.sched.startJobOnceFree(ctx, func(ctx context.Context) { - s.startAdHocScript(ctx, filename, errChan) - }) -} - -func (s *Service) startAdHocScript(ctx context.Context, filename string, errChan chan error) { - defer close(errChan) - - code, err := s.readScript(filename, true) - if err != nil { - errChan <- errors.Wrapf(err, "cannot load script file %v", filename) - return - } - - ctx = ctxWithScriptEnv(ctx, scriptEnv{filename: filepath.Base(filename)}) - - if _, err := risor.Eval(ctx, code, - risor.WithGlobals(s.builtins()), - ); err != nil { - errChan <- errors.Wrapf(err, "script %v", filename) - return - } -} - -type loadedScriptResult struct { - scriptPlugin *ScriptPlugin - err error -} - -func (s *Service) loadScript(ctx context.Context, filename string, resChan chan loadedScriptResult) { - defer close(resChan) - - code, err := s.readScript(filename, false) - if err != nil { - resChan <- loadedScriptResult{err: errors.Wrapf(err, "cannot load script file %v", filename)} - return - } - - newPlugin := &ScriptPlugin{ - name: strings.TrimSuffix(filepath.Base(filename), filepath.Ext(filename)), - scriptService: s, - } - - ctx = ctxWithScriptEnv(ctx, scriptEnv{filename: filepath.Base(filename)}) - - if _, err := risor.Eval(ctx, code, - risor.WithGlobals(s.builtins()), - risor.WithGlobals(map[string]any{ - "ext": (&extModule{scriptPlugin: newPlugin}).register(), - }), - ); err != nil { - resChan <- loadedScriptResult{err: errors.Wrapf(err, "script %v", filename)} - return - } - - resChan <- loadedScriptResult{scriptPlugin: newPlugin} -} - -func (s *Service) readScript(filename string, allowCwd bool) (string, 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 "", err - } - return string(code), nil - } - } else { - log.Printf("warn: cannot get cwd for reading script %v: %v", filename, err) - } - } - - if strings.HasPrefix(filename, string(filepath.Separator)) || strings.HasPrefix(filename, relPrefix) { - code, err := os.ReadFile(filename) - if err != nil { - return "", err - } - return string(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) { - continue - } else { - return "", err - } - } else if stat.IsDir() { - continue - } - - code, err := fs.ReadFile(currFS, filename) - if err == nil { - return string(code), nil - } else { - return "", err - } - } - - return "", os.ErrNotExist -} - -// LookupCommand looks up a command defined by a script. -// TODO: Command should probably accept/return a chan error to indicate that this will run in a separate goroutine -func (s *Service) LookupCommand(name string) *Command { - for _, p := range s.plugins { - if cmd, hasCmd := p.definedCommands[name]; hasCmd { - return cmd - } - } - return nil -} - -func (s *Service) LookupKeyBinding(key string) (string, *Command) { - for _, p := range s.plugins { - if bindingName, hasBinding := p.keyToKeyBinding[key]; hasBinding { - if cmd, hasCmd := p.definedKeyBindings[bindingName]; hasCmd { - return bindingName, cmd - } - } - } - return "", nil -} - -func (s *Service) UnbindKey(key string) { - for _, p := range s.plugins { - if _, hasBinding := p.keyToKeyBinding[key]; hasBinding { - delete(p.keyToKeyBinding, key) - } - } -} - -func (s *Service) RebindKeyBinding(keyBinding string, newKey string) error { - if newKey == "" { - for _, p := range s.plugins { - for k, b := range p.keyToKeyBinding { - if b == keyBinding { - delete(p.keyToKeyBinding, k) - } - } - } - return nil - } - - for _, p := range s.plugins { - if _, hasCmd := p.definedKeyBindings[keyBinding]; hasCmd { - if newKey != "" { - p.keyToKeyBinding[newKey] = keyBinding - } - return nil - } - } - - return keybindings.InvalidBindingError(keyBinding) -} - -func (s *Service) builtins() map[string]any { - return map[string]any{ - "ui": (&uiModule{uiService: s.ifaces.UI}).register(), - "session": (&sessionModule{sessionService: s.ifaces.Session}).register(), - "print": object.NewBuiltin("print", printBuiltin), - "printf": object.NewBuiltin("printf", printfBuiltin), - } -} diff --git a/internal/dynamo-browse/services/scriptmanager/service_test.go b/internal/dynamo-browse/services/scriptmanager/service_test.go deleted file mode 100644 index 745124d..0000000 --- a/internal/dynamo-browse/services/scriptmanager/service_test.go +++ /dev/null @@ -1,150 +0,0 @@ -package scriptmanager_test - -import ( - "context" - "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/scriptmanager" - "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/scriptmanager/mocks" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "io/fs" - "testing" - "testing/fstest" - "time" -) - -func TestService_RunAdHocScript(t *testing.T) { - t.Run("successfully loads and executes a script", func(t *testing.T) { - testFS := testScriptFile(t, "test.tm", ` - ui.print("Hello, world") - `) - - mockedUIService := mocks.NewUIService(t) - mockedUIService.EXPECT().PrintMessage(mock.Anything, "Hello, world") - - srv := scriptmanager.New(scriptmanager.WithFS(testFS)) - srv.SetIFaces(scriptmanager.Ifaces{ - UI: mockedUIService, - }) - - ctx := context.Background() - err := <-srv.RunAdHocScript(ctx, "test.tm") - assert.NoError(t, err) - - mockedUIService.AssertExpectations(t) - }) -} - -func TestService_LoadScript(t *testing.T) { - t.Run("successfully loads a script and exposes it as a plugin", func(t *testing.T) { - testFS := testScriptFile(t, "test.tm", ` - ext.command("somewhere", func(a) { - ui.print("Hello, " + a) - }) - `) - - ctx := context.Background() - - mockedUIService := mocks.NewUIService(t) - mockedUIService.EXPECT().PrintMessage(mock.Anything, "Hello, someone") - - srv := scriptmanager.New(scriptmanager.WithFS(testFS)) - srv.SetIFaces(scriptmanager.Ifaces{ - UI: mockedUIService, - }) - - plugin, err := srv.LoadScript(ctx, "test.tm") - assert.NoError(t, err) - assert.NotNil(t, plugin) - assert.Equal(t, "test", plugin.Name()) - - cmd := srv.LookupCommand("somewhere") - assert.NotNil(t, cmd) - - errChan := make(chan error) - err = cmd.Invoke(ctx, []string{"someone"}, errChan) - assert.NoError(t, err) - assert.NoError(t, waitForErr(t, errChan)) - - mockedUIService.AssertExpectations(t) - }) - - t.Run("reloading a script with the same name should remove the old one", func(t *testing.T) { - testFS := fstest.MapFS{ - "test.tm": &fstest.MapFile{ - Data: []byte(` - ext.command("somewhere", func(a) { - ui.print("Hello, " + a) - }) - `), - }, - } - - ctx := context.Background() - - mockedUIService := mocks.NewUIService(t) - mockedUIService.EXPECT().PrintMessage(mock.Anything, "Hello, someone").Once() - mockedUIService.EXPECT().PrintMessage(mock.Anything, "Goodbye, someone").Once() - - srv := scriptmanager.New(scriptmanager.WithFS(testFS)) - srv.SetIFaces(scriptmanager.Ifaces{ - UI: mockedUIService, - }) - - // Execute the old script - _, err := srv.LoadScript(ctx, "test.tm") - assert.NoError(t, err) - - cmd := srv.LookupCommand("somewhere") - assert.NotNil(t, cmd) - - errChan := make(chan error) - err = cmd.Invoke(ctx, []string{"someone"}, errChan) - assert.NoError(t, err) - assert.NoError(t, waitForErr(t, errChan)) - - // Change the script and reload - testFS["test.tm"] = &fstest.MapFile{ - Data: []byte(` - ext.command("somewhere", func(a) { - ui.print("Goodbye, " + a) - }) - `), - } - - _, err = srv.LoadScript(ctx, "test.tm") - assert.NoError(t, err) - - cmd = srv.LookupCommand("somewhere") - assert.NotNil(t, cmd) - - errChan = make(chan error) - err = cmd.Invoke(ctx, []string{"someone"}, errChan) - assert.NoError(t, err) - assert.NoError(t, waitForErr(t, errChan)) - - mockedUIService.AssertExpectations(t) - }) -} - -func testScriptFile(t *testing.T, filename, code string) fs.FS { - t.Helper() - - testFs := fstest.MapFS{ - filename: &fstest.MapFile{ - Data: []byte(code), - }, - } - return testFs -} - -func waitForErr(t *testing.T, errChan chan error) error { - t.Helper() - - select { - case err := <-errChan: - return err - case <-time.After(5 * time.Second): - t.Fatalf("timed-out waiting for an error") - } - return nil -} diff --git a/internal/dynamo-browse/services/scriptmanager/serviceopts.go b/internal/dynamo-browse/services/scriptmanager/serviceopts.go deleted file mode 100644 index 6841531..0000000 --- a/internal/dynamo-browse/services/scriptmanager/serviceopts.go +++ /dev/null @@ -1,11 +0,0 @@ -package scriptmanager - -import "io/fs" - -type ServiceOption func(srv *Service) - -func WithFS(fs ...fs.FS) ServiceOption { - return func(srv *Service) { - srv.lookupPaths = fs - } -} diff --git a/internal/dynamo-browse/services/scriptmanager/tableproxy.go b/internal/dynamo-browse/services/scriptmanager/tableproxy.go deleted file mode 100644 index 252348f..0000000 --- a/internal/dynamo-browse/services/scriptmanager/tableproxy.go +++ /dev/null @@ -1,138 +0,0 @@ -package scriptmanager - -import ( - "github.com/lmika/dynamo-browse/internal/common/sliceutils" - "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" - "github.com/pkg/errors" - "github.com/risor-io/risor/object" - "github.com/risor-io/risor/op" - "reflect" -) - -const ( - tableProxyPartitionKey = "hash" - tableProxySortKey = "range" -) - -type tableProxy struct { - table *models.TableInfo -} - -func (t *tableProxy) SetAttr(name string, value object.Object) error { - return errors.Errorf("attribute error: %v", name) -} - -func (t *tableProxy) RunOperation(opType op.BinaryOpType, right object.Object) object.Object { - return object.Errorf("op error: unsupported %v", opType) -} - -func (t *tableProxy) Cost() int { - return 0 -} - -func (t *tableProxy) Type() object.Type { - return "table" -} - -func (t *tableProxy) Inspect() string { - return "table(" + t.table.Name + ")" -} - -func (t *tableProxy) Interface() interface{} { - return t.table -} - -func (t *tableProxy) Equals(other object.Object) object.Object { - otherT, isOtherRS := other.(*tableProxy) - if !isOtherRS { - return object.False - } - - return object.NewBool(reflect.DeepEqual(t.table, otherT.table)) -} - -func (t *tableProxy) GetAttr(name string) (object.Object, bool) { - switch name { - 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), - }), true - case "gsis": - return object.NewList(sliceutils.Map(t.table.GSIs, newTableIndexProxy)), true - } - - return nil, false -} - -func (t *tableProxy) IsTruthy() bool { - return true -} - -type tableIndexProxy struct { - gsi models.TableGSI -} - -func (t tableIndexProxy) SetAttr(name string, value object.Object) error { - return errors.Errorf("attribute error: %v", name) -} - -func (t tableIndexProxy) RunOperation(opType op.BinaryOpType, right object.Object) object.Object { - return object.Errorf("op error: unsupported %v", opType) -} - -func (t tableIndexProxy) Cost() int { - return 0 -} - -func newTableIndexProxy(gsi models.TableGSI) object.Object { - return tableIndexProxy{gsi: gsi} -} - -func (t tableIndexProxy) Type() object.Type { - return "table_index" -} - -func (t tableIndexProxy) Inspect() string { - return "table_index(gsi," + t.gsi.Name + ")" -} - -func (t tableIndexProxy) Interface() interface{} { - return t.gsi -} - -func (t tableIndexProxy) Equals(other object.Object) object.Object { - otherIP, isOtherIP := other.(tableIndexProxy) - if !isOtherIP { - return object.False - } - - return object.NewBool(reflect.DeepEqual(t.gsi, otherIP.gsi)) -} - -func (t tableIndexProxy) GetAttr(name string) (object.Object, bool) { - switch name { - case "name": - return object.NewString(t.gsi.Name), true - case "keys": - return object.NewMap(map[string]object.Object{ - tableProxyPartitionKey: object.NewString(t.gsi.Keys.PartitionKey), - tableProxySortKey: object.NewString(t.gsi.Keys.SortKey), - }), true - } - - return nil, false -} - -func (t tableIndexProxy) IsTruthy() bool { - return true -} diff --git a/internal/dynamo-browse/services/scriptmanager/typemapping.go b/internal/dynamo-browse/services/scriptmanager/typemapping.go deleted file mode 100644 index d7b8a47..0000000 --- a/internal/dynamo-browse/services/scriptmanager/typemapping.go +++ /dev/null @@ -1,135 +0,0 @@ -package scriptmanager - -import ( - "fmt" - "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" - "github.com/lmika/dynamo-browse/internal/common/maputils" - "github.com/lmika/dynamo-browse/internal/common/sliceutils" - "github.com/pkg/errors" - "github.com/risor-io/risor/object" - "regexp" - "strconv" -) - -func tamarinValueToAttributeValue(val object.Object) (types.AttributeValue, error) { - switch v := val.(type) { - case *object.String: - return &types.AttributeValueMemberS{Value: v.Value()}, nil - case *object.Int: - return &types.AttributeValueMemberN{Value: strconv.FormatInt(v.Value(), 10)}, nil - case *object.Float: - return &types.AttributeValueMemberN{Value: strconv.FormatFloat(v.Value(), 'f', -1, 64)}, nil - case *object.Bool: - return &types.AttributeValueMemberBOOL{Value: v.Value()}, nil - case *object.NilType: - return &types.AttributeValueMemberNULL{Value: true}, nil - case *object.List: - attrValue, err := sliceutils.MapWithError(v.Value(), tamarinValueToAttributeValue) - if err != nil { - return nil, err - } - return &types.AttributeValueMemberL{Value: attrValue}, nil - case *object.Map: - attrValue, err := maputils.MapValuesWithError(v.Value(), tamarinValueToAttributeValue) - if err != nil { - return nil, err - } - return &types.AttributeValueMemberM{Value: attrValue}, nil - case *object.Set: - values := maputils.Values(v.Value()) - canBeNumSet := sliceutils.All(values, func(t object.Object) bool { - _, isInt := t.(*object.Int) - _, isFloat := t.(*object.Float) - return isInt || isFloat - }) - - if canBeNumSet { - return &types.AttributeValueMemberNS{ - Value: sliceutils.Map(values, func(t object.Object) string { - switch v := t.(type) { - case *object.Int: - return strconv.FormatInt(v.Value(), 10) - case *object.Float: - return strconv.FormatFloat(v.Value(), 'f', -1, 64) - } - panic(fmt.Sprintf("unhandled object type: %v", t.Type())) - }), - }, nil - } - return &types.AttributeValueMemberSS{ - Value: sliceutils.Map(values, func(t object.Object) string { - v, _ := object.AsString(t) - return v - }), - }, nil - } - return nil, errors.Errorf("type error: unsupported value type (got %v)", val.Type()) -} - -func attributeValueToTamarin(val types.AttributeValue) (object.Object, error) { - if val == nil { - return object.Nil, nil - } - - switch v := val.(type) { - case *types.AttributeValueMemberS: - return object.NewString(v.Value), nil - case *types.AttributeValueMemberN: - f, err := convertNumAttributeToTamarinValue(v.Value) - if err != nil { - return nil, errors.Errorf("value error: invalid N value: %v", v.Value) - } - return f, nil - case *types.AttributeValueMemberBOOL: - if v.Value { - return object.True, nil - } - return object.False, nil - case *types.AttributeValueMemberNULL: - return object.Nil, nil - case *types.AttributeValueMemberL: - list, err := sliceutils.MapWithError(v.Value, attributeValueToTamarin) - if err != nil { - return nil, err - } - return object.NewList(list), nil - case *types.AttributeValueMemberM: - objMap, err := maputils.MapValuesWithError(v.Value, attributeValueToTamarin) - if err != nil { - return nil, err - } - return object.NewMap(objMap), nil - case *types.AttributeValueMemberSS: - return object.NewSet(sliceutils.Map(v.Value, func(s string) object.Object { - return object.NewString(s) - })), nil - case *types.AttributeValueMemberNS: - nums, err := sliceutils.MapWithError(v.Value, func(s string) (object.Object, error) { - return convertNumAttributeToTamarinValue(s) - }) - if err != nil { - return nil, err - } - return object.NewSet(nums), nil - } - return nil, errors.Errorf("value error: cannot convert type %T to tamarin object", val) -} - -var intNumberPattern = regexp.MustCompile(`^[-]?[0-9]+$`) - -// XXX - this is pretty crappy in that it does not support large values -func convertNumAttributeToTamarinValue(n string) (object.Object, error) { - if intNumberPattern.MatchString(n) { - parsedInt, err := strconv.ParseInt(n, 10, 64) - if err != nil { - return nil, err - } - return object.NewInt(parsedInt), nil - } - - f, err := strconv.ParseFloat(n, 64) - if err != nil { - return nil, err - } - return object.NewFloat(f), nil -} diff --git a/internal/dynamo-browse/services/scriptmanager/types.go b/internal/dynamo-browse/services/scriptmanager/types.go deleted file mode 100644 index 442f03b..0000000 --- a/internal/dynamo-browse/services/scriptmanager/types.go +++ /dev/null @@ -1,35 +0,0 @@ -package scriptmanager - -import ( - "context" -) - -type ScriptPlugin struct { - scriptService *Service - name string - definedCommands map[string]*Command - definedKeyBindings map[string]*Command - keyToKeyBinding map[string]string - relatedItems []*relatedItemBuilder -} - -func (sp *ScriptPlugin) Name() string { - return sp.name -} - -type Command struct { - plugin *ScriptPlugin - cmdFn func(ctx context.Context, args []string) error -} - -// Invoke will schedule the command for invocation. If the script scheduler is free, it will be started immediately. -// Otherwise an error will be returned. -func (c *Command) Invoke(ctx context.Context, args []string, errChan chan error) error { - return c.plugin.scriptService.sched.runNow(ctx, func(ctx context.Context) { - errChan <- c.cmdFn(ctx, args) - }) -} - -//func (c *Command) LookupRelevantItems(ctx context.Context, table *models.TableInfo, item *models.Item) error { -// -//} diff --git a/internal/dynamo-browse/ui/model.go b/internal/dynamo-browse/ui/model.go index 42f7e31..fab7026 100644 --- a/internal/dynamo-browse/ui/model.go +++ b/internal/dynamo-browse/ui/model.go @@ -85,172 +85,12 @@ func NewModel( dialogPrompt := dialogprompt.New(statusAndPrompt) tableSelect := tableselect.New(dialogPrompt, uiStyles) - /* - cc.AddCommands(&commandctrl.CommandList{ - Commands: map[string]commandctrl.Command{ - "quit": commandctrl.NoArgCommand(tea.Quit), - "table": func(ctx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg { - var tableName string - if err := args.Bind(&tableName); err == nil { - return rc.ScanTable(tableName) - } - - return rc.ListTables(false) - }, - "export": func(ctx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg { - var filename string - if err := args.Bind(&filename); err != nil { - return events.Error(errors.New("expected filename")) - } - - opts := controllers.ExportOptions{ - AllResults: args.HasSwitch("all"), - } - - return exportController.ExportCSV(filename, opts) - }, - "mark": func(ctx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg { - var markOp = controllers.MarkOpMark - - var markOpStr string - if err := args.Bind(&markOpStr); err == nil { - switch markOpStr { - case "all": - markOp = controllers.MarkOpMark - case "none": - markOp = controllers.MarkOpUnmark - case "toggle": - markOp = controllers.MarkOpToggle - default: - return events.Error(errors.New("unrecognised mark operation")) - } - } - - var whereExpr = "" - _ = args.BindSwitch("where", &whereExpr) - - return rc.Mark(markOp, whereExpr) - }, - "unmark": func(ctx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg { - return rc.Mark(controllers.MarkOpUnmark, "") - }, - "next-page": func(ctx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg { - return rc.NextPage() - }, - "delete": commandctrl.NoArgCommand(wc.DeleteMarked), - - // TEMP - "new-item": commandctrl.NoArgCommand(wc.NewItem), - "clone": func(ctx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg { - return wc.CloneItem(dtv.SelectedItemIndex()) - }, - "set-attr": func(ctx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg { - var fieldName string - if err := args.Bind(&fieldName); err != nil { - return events.Error(errors.New("expected field")) - } - - var itemType = models.UnsetItemType - switch { - case args.HasSwitch("S"): - itemType = models.StringItemType - case args.HasSwitch("N"): - itemType = models.NumberItemType - case args.HasSwitch("BOOL"): - itemType = models.BoolItemType - case args.HasSwitch("NULL"): - itemType = models.NullItemType - case args.HasSwitch("TO"): - itemType = models.ExprValueItemType - } - - return wc.SetAttributeValue(dtv.SelectedItemIndex(), itemType, fieldName) - }, - "del-attr": func(ctx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg { - var fieldName string - // TODO: support rest args - if err := args.Bind(&fieldName); err != nil { - return events.Error(errors.New("expected field")) - } - - return wc.DeleteAttribute(dtv.SelectedItemIndex(), fieldName) - }, - - "put": func(ctx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg { - return wc.PutItems() - }, - "touch": func(ctx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg { - return wc.TouchItem(dtv.SelectedItemIndex()) - }, - "noisy-touch": func(ctx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg { - return wc.NoisyTouchItem(dtv.SelectedItemIndex()) - }, - - - "echo": func(ctx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg { - s := new(strings.Builder) - for _, arg := range args { - s.WriteString(arg) - } - return events.StatusMsg(s.String()) - }, - "set-opt": func(ctx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg { - var name string - if err := args.Bind(&name); err != nil { - return events.Error(errors.New("expected settingName")) - } - - var value string - if err := args.Bind(&value); err == nil { - return settingsController.SetSetting(name, value) - } - - return settingsController.SetSetting(name, "") - }, - "rebind": func(ctx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg { - var bindingName, newKey string - if err := args.Bind(&bindingName, &newKey); err != nil { - return events.Error(errors.New("expected: bindingName newKey")) - } - - return keyBindingController.Rebind(bindingName, newKey, ctx.FromFile) - }, - - "run-script": func(ctx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg { - var name string - if err := args.Bind(&name); err != nil { - return events.Error(errors.New("expected: script name")) - } - - return scriptController.RunScript(name) - }, - "load-script": func(ctx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg { - var name string - if err := args.Bind(&name); err != nil { - return events.Error(errors.New("expected: script name")) - } - - return scriptController.LoadScript(name) - }, - - // Aliases - "sa": cc.Alias("set-attr"), - "da": cc.Alias("del-attr"), - "np": cc.Alias("next-page"), - "w": cc.Alias("put"), - "q": cc.Alias("quit"), - }, - }) - - */ - root := layout.FullScreen(tableSelect) return Model{ tableReadController: rc, tableWriteController: wc, commandController: cc, - //scriptController: scriptController, exportController: exportController, jobController: jobController, itemEdit: itemEdit,