sqs-browse: started working on put commands
This commit is contained in:
		
							parent
							
								
									43680000a8
								
							
						
					
					
						commit
						cecdbafabb
					
				|  | @ -7,7 +7,9 @@ import ( | |||
| 	"github.com/aws/aws-sdk-go-v2/config" | ||||
| 	"github.com/aws/aws-sdk-go-v2/service/dynamodb" | ||||
| 	tea "github.com/charmbracelet/bubbletea" | ||||
| 	"github.com/lmika/awstools/internal/common/ui/commandctrl" | ||||
| 	"github.com/lmika/awstools/internal/common/ui/dispatcher" | ||||
| 	"github.com/lmika/awstools/internal/common/ui/uimodels" | ||||
| 	"github.com/lmika/awstools/internal/dynamo-browse/controllers" | ||||
| 	"github.com/lmika/awstools/internal/dynamo-browse/providers/dynamo" | ||||
| 	"github.com/lmika/awstools/internal/dynamo-browse/services/tables" | ||||
|  | @ -45,7 +47,13 @@ func main() { | |||
| 	tableReadController := controllers.NewTableReadController(tableService, *flagTable) | ||||
| 	tableWriteController := controllers.NewTableWriteController(tableService, tableReadController, *flagTable) | ||||
| 
 | ||||
| 	uiModel := ui.NewModel(uiDispatcher, tableReadController, tableWriteController) | ||||
| 	commandController := commandctrl.NewCommandController(map[string]uimodels.Operation{ | ||||
| 		"scan": tableReadController.Scan(), | ||||
| 		"rw": tableWriteController.ToggleReadWrite(), | ||||
| 		"dup": tableWriteController.Duplicate(), | ||||
| 	}) | ||||
| 
 | ||||
| 	uiModel := ui.NewModel(uiDispatcher, commandController, tableReadController, tableWriteController) | ||||
| 	p := tea.NewProgram(uiModel, tea.WithAltScreen()) | ||||
| 	loopback.program = p | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										2
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								go.mod
									
									
									
									
									
								
							|  | @ -20,6 +20,7 @@ require ( | |||
| ) | ||||
| 
 | ||||
| require ( | ||||
| 	github.com/alecthomas/participle/v2 v2.0.0-alpha7 // indirect | ||||
| 	github.com/atotto/clipboard v0.1.4 // indirect | ||||
| 	github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.10.0 // indirect | ||||
| 	github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.6 // indirect | ||||
|  | @ -36,6 +37,7 @@ require ( | |||
| 	github.com/davecgh/go-spew v1.1.1 // indirect | ||||
| 	github.com/jmespath/go-jmespath v0.4.0 // indirect | ||||
| 	github.com/juju/ansiterm v0.0.0-20210929141451-8b71cc96ebdc // indirect | ||||
| 	github.com/lmika/shellwords v0.0.0-20140714114018-ce258dd729fe // 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.14 // indirect | ||||
|  |  | |||
							
								
								
									
										8
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										8
									
								
								go.sum
									
									
									
									
									
								
							|  | @ -1,3 +1,7 @@ | |||
| github.com/alecthomas/participle v0.7.1 h1:2bN7reTw//5f0cugJcTOnY/NYZcWQOaajW+BwZB5xWs= | ||||
| github.com/alecthomas/participle/v2 v2.0.0-alpha7 h1:cK4vjj0VSgb3lN1nuKA5F7dw+1s1pWBe5bx7nNCnN+c= | ||||
| github.com/alecthomas/participle/v2 v2.0.0-alpha7/go.mod h1:NumScqsC42o9x+dGj8/YqsIfhrIQjFEOFovxotbBirA= | ||||
| github.com/alecthomas/repr v0.0.0-20181024024818-d37bc2a10ba1/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ= | ||||
| 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 h1:1XIXAfxsEmbhbj5ry3D3vX+6ZcUYvIqSm4CWWEuGZCA= | ||||
|  | @ -75,6 +79,8 @@ github.com/lmika/events v0.0.0-20200906102219-a2269cd4394e h1:0QkUe2ejnT/i+xbgGy | |||
| github.com/lmika/events v0.0.0-20200906102219-a2269cd4394e/go.mod h1:qtkBmNC9OfD0STtOR9sF55pQchjIfNlC3gzm4n8CrqM= | ||||
| 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/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= | ||||
| github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= | ||||
| github.com/lunixbochs/vtclean v1.0.0 h1:xu2sLAri4lGiovBDQKxl5mrXyESr3gUr5m5SM5+LVb8= | ||||
|  | @ -106,6 +112,7 @@ github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= | |||
| github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= | ||||
| github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= | ||||
| github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= | ||||
| github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= | ||||
| github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= | ||||
| github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= | ||||
| golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
|  | @ -121,6 +128,7 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX | |||
| golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | ||||
| gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | ||||
| 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/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= | ||||
| gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= | ||||
| gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | ||||
|  |  | |||
							
								
								
									
										48
									
								
								internal/common/ui/commandctrl/commandctrl.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								internal/common/ui/commandctrl/commandctrl.go
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,48 @@ | |||
| package commandctrl | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"github.com/lmika/awstools/internal/common/ui/events" | ||||
| 	"github.com/lmika/awstools/internal/common/ui/uimodels" | ||||
| 	"github.com/lmika/shellwords" | ||||
| 	"github.com/pkg/errors" | ||||
| 	"strings" | ||||
| ) | ||||
| 
 | ||||
| type CommandController struct { | ||||
| 	commands map[string]uimodels.Operation | ||||
| } | ||||
| 
 | ||||
| func NewCommandController(commands map[string]uimodels.Operation) *CommandController { | ||||
| 	return &CommandController{ | ||||
| 		commands: commands, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (c *CommandController) Prompt() uimodels.Operation { | ||||
| 	return uimodels.OperationFn(func(ctx context.Context) error { | ||||
| 		uiCtx := uimodels.Ctx(ctx) | ||||
| 		uiCtx.Send(events.PromptForInput{ | ||||
| 			Prompt: ":", | ||||
| 			OnDone: c.Execute(), | ||||
| 		}) | ||||
| 		return nil | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| func (c *CommandController) Execute() uimodels.Operation { | ||||
| 	return uimodels.OperationFn(func(ctx context.Context) error { | ||||
| 		input := strings.TrimSpace(uimodels.PromptValue(ctx)) | ||||
| 		if input == "" { | ||||
| 			return nil | ||||
| 		} | ||||
| 
 | ||||
| 		tokens := shellwords.Split(input) | ||||
| 		command, ok := c.commands[tokens[0]] | ||||
| 		if !ok { | ||||
| 			return errors.New("no such command: " + tokens[0]) | ||||
| 		} | ||||
| 
 | ||||
| 		return command.Execute(WithCommandArgs(ctx, tokens[1:])) | ||||
| 	}) | ||||
| } | ||||
							
								
								
									
										25
									
								
								internal/common/ui/commandctrl/commandctrl_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								internal/common/ui/commandctrl/commandctrl_test.go
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,25 @@ | |||
| package commandctrl_test | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"github.com/lmika/awstools/internal/common/ui/commandctrl" | ||||
| 	"github.com/lmika/awstools/internal/common/ui/events" | ||||
| 	"github.com/lmika/awstools/test/testuictx" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| 	"testing" | ||||
| ) | ||||
| 
 | ||||
| func TestCommandController_Prompt(t *testing.T) { | ||||
| 	t.Run("prompt user for a command", func(t *testing.T) { | ||||
| 		cmd := commandctrl.NewCommandController() | ||||
| 
 | ||||
| 		ctx, uiCtx := testuictx.New(context.Background()) | ||||
| 		err := cmd.Prompt().Execute(ctx) | ||||
| 
 | ||||
| 		assert.NoError(t, err) | ||||
| 
 | ||||
| 		promptMsg, ok := uiCtx.Messages[0].(events.PromptForInput) | ||||
| 		assert.True(t, ok) | ||||
| 		assert.Equal(t, ":", promptMsg.Prompt) | ||||
| 	}) | ||||
| } | ||||
							
								
								
									
										15
									
								
								internal/common/ui/commandctrl/context.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								internal/common/ui/commandctrl/context.go
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,15 @@ | |||
| package commandctrl | ||||
| 
 | ||||
| import "context" | ||||
| 
 | ||||
| type commandArgContextKeyType struct {} | ||||
| var commandArgContextKey = commandArgContextKeyType{} | ||||
| 
 | ||||
| func WithCommandArgs(ctx context.Context, args []string) context.Context { | ||||
| 	return context.WithValue(ctx, commandArgContextKey, args) | ||||
| } | ||||
| 
 | ||||
| func CommandArgs(ctx context.Context) []string { | ||||
| 	args, _ := ctx.Value(commandArgContextKey).([]string) | ||||
| 	return args | ||||
| } | ||||
|  | @ -3,6 +3,7 @@ package controllers | |||
| import ( | ||||
| 	"context" | ||||
| 	"github.com/lmika/awstools/internal/common/ui/uimodels" | ||||
| 	"github.com/lmika/awstools/internal/dynamo-browse/models/modexpr" | ||||
| 	"github.com/lmika/awstools/internal/dynamo-browse/services/tables" | ||||
| 	"github.com/pkg/errors" | ||||
| ) | ||||
|  | @ -21,12 +22,67 @@ func NewTableWriteController(tableService *tables.Service, tableReadControllers | |||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (c *TableWriteController) EnableReadWrite() uimodels.Operation { | ||||
| func (c *TableWriteController) ToggleReadWrite() uimodels.Operation { | ||||
| 	return uimodels.OperationFn(func(ctx context.Context) error { | ||||
| 		uiCtx := uimodels.Ctx(ctx) | ||||
| 		uiCtx.Send(SetReadWrite{NewValue: true}) | ||||
| 		uiCtx.Message("read/write mode enabled") | ||||
| 		state := CurrentState(ctx) | ||||
| 
 | ||||
| 		if state.InReadWriteMode { | ||||
| 			uiCtx.Send(SetReadWrite{NewValue: false}) | ||||
| 			uiCtx.Message("read/write mode disabled") | ||||
| 		} else { | ||||
| 			uiCtx.Send(SetReadWrite{NewValue: true}) | ||||
| 			uiCtx.Message("read/write mode enabled") | ||||
| 		} | ||||
| 
 | ||||
| 		return nil | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| func (c *TableWriteController) Duplicate() uimodels.Operation { | ||||
| 	return uimodels.OperationFn(func(ctx context.Context) error { | ||||
| 		uiCtx := uimodels.Ctx(ctx) | ||||
| 		state := CurrentState(ctx) | ||||
| 
 | ||||
| 		if state.SelectedItem == nil { | ||||
| 			return errors.New("no selected item") | ||||
| 		} else if !state.InReadWriteMode { | ||||
| 			return errors.New("not in read/write mode") | ||||
| 		} | ||||
| 
 | ||||
| 		uiCtx.Input("Dup: ", uimodels.OperationFn(func(ctx context.Context) error { | ||||
| 			modExpr, err := modexpr.Parse(uimodels.PromptValue(ctx)) | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 
 | ||||
| 			newItem, err := modExpr.Patch(state.SelectedItem) | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 
 | ||||
| 			// TODO: preview new item
 | ||||
| 
 | ||||
| 			uiCtx := uimodels.Ctx(ctx) | ||||
| 			uiCtx.Input("Put item? ", uimodels.OperationFn(func(ctx context.Context) error { | ||||
| 				if uimodels.PromptValue(ctx) != "y" { | ||||
| 					return errors.New("operation aborted") | ||||
| 				} | ||||
| 
 | ||||
| 				// Delete the item
 | ||||
| 				if err := c.tableService.Put(ctx, c.tableName, newItem); err != nil { | ||||
| 					return err | ||||
| 				} | ||||
| 
 | ||||
| 				// Rescan to get updated items
 | ||||
| 				if err := c.tableReadControllers.doScan(ctx, true); err != nil { | ||||
| 					return err | ||||
| 				} | ||||
| 
 | ||||
| 				return nil | ||||
| 			})) | ||||
| 			return nil | ||||
| 		})) | ||||
| 		return nil | ||||
| 	}) | ||||
| } | ||||
|  |  | |||
|  | @ -13,21 +13,33 @@ import ( | |||
| 	"testing" | ||||
| ) | ||||
| 
 | ||||
| func TestTableWriteController_EnableReadWrite(t *testing.T) { | ||||
| func TestTableWriteController_ToggleReadWrite(t *testing.T) { | ||||
| 	twc, _, closeFn := setupController(t) | ||||
| 	t.Cleanup(closeFn) | ||||
| 
 | ||||
| 	t.Run("should send event enabling read write", func(t *testing.T) { | ||||
| 	t.Run("should enabling read write if disabled", func(t *testing.T) { | ||||
| 		ctx, uiCtx := testuictx.New(context.Background()) | ||||
| 		ctx = controllers.ContextWithState(ctx, controllers.State{ | ||||
| 			InReadWriteMode: false, | ||||
| 		}) | ||||
| 
 | ||||
| 		err := twc.EnableReadWrite().Execute(ctx) | ||||
| 		err := twc.ToggleReadWrite().Execute(ctx) | ||||
| 		assert.NoError(t, err) | ||||
| 
 | ||||
| 		assert.Contains(t, uiCtx.Messages, controllers.SetReadWrite{NewValue: true}) | ||||
| 	}) | ||||
| 
 | ||||
| 	t.Run("should disable read write if enabled", func(t *testing.T) { | ||||
| 		ctx, uiCtx := testuictx.New(context.Background()) | ||||
| 		ctx = controllers.ContextWithState(ctx, controllers.State{ | ||||
| 			InReadWriteMode: true, | ||||
| 		}) | ||||
| 
 | ||||
| 		err := twc.ToggleReadWrite().Execute(ctx) | ||||
| 		assert.NoError(t, err) | ||||
| 
 | ||||
| 		assert.Contains(t, uiCtx.Messages, controllers.SetReadWrite{NewValue: false}) | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| func TestTableWriteController_Delete(t *testing.T) { | ||||
|  |  | |||
|  | @ -9,3 +9,15 @@ type ResultSet struct { | |||
| } | ||||
| 
 | ||||
| type Item map[string]types.AttributeValue | ||||
| 
 | ||||
| // Clone creates a clone of the current item
 | ||||
| func (i Item) Clone() Item { | ||||
| 	newItem := Item{} | ||||
| 
 | ||||
| 	// TODO: should be a deep clone?
 | ||||
| 	for k, v := range i { | ||||
| 		newItem[k] = v | ||||
| 	} | ||||
| 
 | ||||
| 	return newItem | ||||
| } | ||||
|  |  | |||
							
								
								
									
										39
									
								
								internal/dynamo-browse/models/modexpr/ast.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								internal/dynamo-browse/models/modexpr/ast.go
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,39 @@ | |||
| package modexpr | ||||
| 
 | ||||
| import ( | ||||
| 	"github.com/alecthomas/participle/v2" | ||||
| 	"github.com/aws/aws-sdk-go-v2/service/dynamodb/types" | ||||
| 	"github.com/pkg/errors" | ||||
| 	"strconv" | ||||
| ) | ||||
| 
 | ||||
| type astExpr struct { | ||||
| 	Attributes []*astAttribute `parser:"@@ (',' @@)*"` | ||||
| } | ||||
| 
 | ||||
| type astAttribute struct { | ||||
| 	Name  string `parser:"@Ident '='"` | ||||
| 	Value string `parser:"@String"` | ||||
| } | ||||
| 
 | ||||
| func (a astAttribute) dynamoValue() (types.AttributeValue, error) { | ||||
| 	// TODO: should be based on type
 | ||||
| 	s, err := strconv.Unquote(a.Value) | ||||
| 	if err != nil { | ||||
| 		return nil, errors.Wrap(err, "cannot unquote string") | ||||
| 	} | ||||
| 	return &types.AttributeValueMemberS{Value: s}, nil | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| var parser = participle.MustBuild(&astExpr{}) | ||||
| 
 | ||||
| func Parse(expr string) (*ModExpr, error) { | ||||
| 	var ast astExpr | ||||
| 
 | ||||
| 	if err := parser.ParseString("expr", expr, &ast); err != nil { | ||||
| 		return nil, errors.Wrapf(err, "cannot parse expression: '%v'", expr) | ||||
| 	} | ||||
| 
 | ||||
| 	return &ModExpr{ast: &ast}, nil | ||||
| } | ||||
							
								
								
									
										22
									
								
								internal/dynamo-browse/models/modexpr/expr.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								internal/dynamo-browse/models/modexpr/expr.go
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,22 @@ | |||
| package modexpr | ||||
| 
 | ||||
| import "github.com/lmika/awstools/internal/dynamo-browse/models" | ||||
| 
 | ||||
| type ModExpr struct { | ||||
| 	ast *astExpr | ||||
| } | ||||
| 
 | ||||
| func (me *ModExpr) Patch(item models.Item) (models.Item, error) { | ||||
| 	newItem := item.Clone() | ||||
| 
 | ||||
| 	for _, attribute := range me.ast.Attributes { | ||||
| 		var err error | ||||
| 		name := attribute.Name | ||||
| 		newItem[name], err = attribute.dynamoValue() | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return newItem, nil | ||||
| } | ||||
							
								
								
									
										39
									
								
								internal/dynamo-browse/models/modexpr/expr_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								internal/dynamo-browse/models/modexpr/expr_test.go
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,39 @@ | |||
| package modexpr_test | ||||
| 
 | ||||
| import ( | ||||
| 	"github.com/aws/aws-sdk-go-v2/service/dynamodb/types" | ||||
| 	"github.com/lmika/awstools/internal/dynamo-browse/models" | ||||
| 	"github.com/lmika/awstools/internal/dynamo-browse/models/modexpr" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| 	"testing" | ||||
| ) | ||||
| 
 | ||||
| func TestModExpr_Patch(t *testing.T) { | ||||
| 	t.Run("patch with new attributes", func(t *testing.T) { | ||||
| 		modExpr, err := modexpr.Parse(`alpha="new value", beta="another new value"`) | ||||
| 		assert.NoError(t, err) | ||||
| 
 | ||||
| 		oldItem := models.Item{} | ||||
| 		newItem, err := modExpr.Patch(oldItem) | ||||
| 		assert.NoError(t, err) | ||||
| 
 | ||||
| 		assert.Equal(t, "new value", newItem["alpha"].(*types.AttributeValueMemberS).Value) | ||||
| 		assert.Equal(t, "another new value", newItem["beta"].(*types.AttributeValueMemberS).Value) | ||||
| 	}) | ||||
| 
 | ||||
| 	t.Run("patch with existing attributes", func(t *testing.T) { | ||||
| 		modExpr, err := modexpr.Parse(`alpha="new value", beta="another new value"`) | ||||
| 		assert.NoError(t, err) | ||||
| 
 | ||||
| 		oldItem := models.Item{ | ||||
| 			"old": &types.AttributeValueMemberS{Value: "before"}, | ||||
| 			"beta": &types.AttributeValueMemberS{Value: "before beta"}, | ||||
| 		} | ||||
| 		newItem, err := modExpr.Patch(oldItem) | ||||
| 		assert.NoError(t, err) | ||||
| 
 | ||||
| 		assert.Equal(t, "before", newItem["old"].(*types.AttributeValueMemberS).Value) | ||||
| 		assert.Equal(t, "new value", newItem["alpha"].(*types.AttributeValueMemberS).Value) | ||||
| 		assert.Equal(t, "another new value", newItem["beta"].(*types.AttributeValueMemberS).Value) | ||||
| 	}) | ||||
| } | ||||
|  | @ -13,6 +13,17 @@ type Provider struct { | |||
| 	client *dynamodb.Client | ||||
| } | ||||
| 
 | ||||
| func (p *Provider) PutItem(ctx context.Context, name string, item models.Item) error { | ||||
| 	_, err := p.client.PutItem(ctx, &dynamodb.PutItemInput{ | ||||
| 		TableName: aws.String(name), | ||||
| 		Item: item, | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		return errors.Wrapf(err, "cannot execute put on table %v", name) | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func NewProvider(client *dynamodb.Client) *Provider { | ||||
| 	return &Provider{client: client} | ||||
| } | ||||
|  |  | |||
|  | @ -9,4 +9,5 @@ import ( | |||
| type TableProvider interface { | ||||
| 	ScanItems(ctx context.Context, tableName string) ([]models.Item, error) | ||||
| 	DeleteItem(ctx context.Context, tableName string, key map[string]types.AttributeValue) error | ||||
| 	PutItem(ctx context.Context, name string, item models.Item) error | ||||
| } | ||||
|  |  | |||
|  | @ -60,6 +60,10 @@ func (s *Service) Scan(ctx context.Context, table string) (*models.ResultSet, er | |||
| 	}, nil | ||||
| } | ||||
| 
 | ||||
| func (s *Service) Put(ctx context.Context, tableName string, item models.Item) error { | ||||
| 	return s.provider.PutItem(ctx, tableName, item) | ||||
| } | ||||
| 
 | ||||
| func (s *Service) Delete(ctx context.Context, name string, item models.Item) error { | ||||
| 	// TODO: do not hardcode keys
 | ||||
| 	return s.provider.DeleteItem(ctx, name, map[string]types.AttributeValue{ | ||||
|  |  | |||
|  | @ -9,6 +9,7 @@ import ( | |||
| 	"github.com/charmbracelet/bubbles/viewport" | ||||
| 	tea "github.com/charmbracelet/bubbletea" | ||||
| 	"github.com/charmbracelet/lipgloss" | ||||
| 	"github.com/lmika/awstools/internal/common/ui/commandctrl" | ||||
| 	"github.com/lmika/awstools/internal/common/ui/dispatcher" | ||||
| 	"github.com/lmika/awstools/internal/common/ui/events" | ||||
| 	"github.com/lmika/awstools/internal/common/ui/uimodels" | ||||
|  | @ -19,13 +20,13 @@ import ( | |||
| 
 | ||||
| var ( | ||||
| 	activeHeaderStyle = lipgloss.NewStyle(). | ||||
| 		Bold(true). | ||||
| 		Foreground(lipgloss.Color("#ffffff")). | ||||
| 		Background(lipgloss.Color("#4479ff")) | ||||
| 				Bold(true). | ||||
| 				Foreground(lipgloss.Color("#ffffff")). | ||||
| 				Background(lipgloss.Color("#4479ff")) | ||||
| 
 | ||||
| 	inactiveHeaderStyle = lipgloss.NewStyle(). | ||||
| 		Foreground(lipgloss.Color("#000000")). | ||||
| 		Background(lipgloss.Color("#d1d1d1")) | ||||
| 				Foreground(lipgloss.Color("#000000")). | ||||
| 				Background(lipgloss.Color("#d1d1d1")) | ||||
| ) | ||||
| 
 | ||||
| type uiModel struct { | ||||
|  | @ -34,20 +35,21 @@ type uiModel struct { | |||
| 
 | ||||
| 	tableWidth, tableHeight int | ||||
| 
 | ||||
| 	ready     bool | ||||
| 	ready bool | ||||
| 	//resultSet *models.ResultSet
 | ||||
| 	state controllers.State | ||||
| 	message   string | ||||
| 	state   controllers.State | ||||
| 	message string | ||||
| 
 | ||||
| 	pendingInput *events.PromptForInput | ||||
| 	textInput    textinput.Model | ||||
| 
 | ||||
| 	dispatcher           *dispatcher.Dispatcher | ||||
| 	commandController    *commandctrl.CommandController | ||||
| 	tableReadController  *controllers.TableReadController | ||||
| 	tableWriteController *controllers.TableWriteController | ||||
| } | ||||
| 
 | ||||
| func NewModel(dispatcher *dispatcher.Dispatcher, tableReadController *controllers.TableReadController, tableWriteController *controllers.TableWriteController) tea.Model { | ||||
| func NewModel(dispatcher *dispatcher.Dispatcher, commandController *commandctrl.CommandController, tableReadController *controllers.TableReadController, tableWriteController *controllers.TableWriteController) tea.Model { | ||||
| 	tbl := table.New([]string{"pk", "sk"}, 100, 20) | ||||
| 	rows := make([]table.Row, 0) | ||||
| 	tbl.SetRows(rows) | ||||
|  | @ -60,6 +62,7 @@ func NewModel(dispatcher *dispatcher.Dispatcher, tableReadController *controller | |||
| 		textInput: textInput, | ||||
| 
 | ||||
| 		dispatcher:           dispatcher, | ||||
| 		commandController:    commandController, | ||||
| 		tableReadController:  tableReadController, | ||||
| 		tableWriteController: tableWriteController, | ||||
| 	} | ||||
|  | @ -174,7 +177,7 @@ func (m uiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { | |||
| 			case "ctrl+c", "esc": | ||||
| 				m.pendingInput = nil | ||||
| 			case "enter": | ||||
| 				m.dispatcher.Start(uimodels.WithPromptValue(context.Background(), m.textInput.Value()), m.pendingInput.OnDone) | ||||
| 				m.invokeOperation(uimodels.WithPromptValue(context.Background(), m.textInput.Value()), m.pendingInput.OnDone) | ||||
| 				m.pendingInput = nil | ||||
| 			default: | ||||
| 				m.textInput, textInputCommands = m.textInput.Update(msg) | ||||
|  | @ -193,12 +196,12 @@ func (m uiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { | |||
| 			m.updateViewportToSelectedMessage() | ||||
| 
 | ||||
| 		// TODO: these should be moved somewhere else
 | ||||
| 		case ":": | ||||
| 			m.invokeOperation(context.Background(), m.commandController.Prompt()) | ||||
| 		case "s": | ||||
| 			m.invokeOperation(m.tableReadController.Scan()) | ||||
| 			m.invokeOperation(context.Background(), m.tableReadController.Scan()) | ||||
| 		case "D": | ||||
| 			m.invokeOperation(m.tableWriteController.Delete()) | ||||
| 		case "w": | ||||
| 			m.invokeOperation(m.tableWriteController.EnableReadWrite()) | ||||
| 			m.invokeOperation(context.Background(), m.tableWriteController.Delete()) | ||||
| 		} | ||||
| 	default: | ||||
| 		m.textInput, textInputCommands = m.textInput.Update(msg) | ||||
|  | @ -213,13 +216,13 @@ func (m uiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { | |||
| 	return m, tea.Batch(textInputCommands, tableMsgs, viewportMsgs) | ||||
| } | ||||
| 
 | ||||
| func (m uiModel) invokeOperation(op uimodels.Operation) { | ||||
| func (m uiModel) invokeOperation(ctx context.Context, op uimodels.Operation) { | ||||
| 	state := m.state | ||||
| 	if selectedItem, ok := m.selectedItem(); ok { | ||||
| 		state.SelectedItem = selectedItem.item | ||||
| 	} | ||||
| 
 | ||||
| 	ctx := controllers.ContextWithState(context.Background(), state) | ||||
| 	ctx = controllers.ContextWithState(ctx, state) | ||||
| 	m.dispatcher.Start(ctx, op) | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,7 +1,9 @@ | |||
| package ui | ||||
| 
 | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"context" | ||||
| 	"encoding/json" | ||||
| 	table "github.com/calyptia/go-bubble-table" | ||||
| 	"github.com/charmbracelet/bubbles/textinput" | ||||
| 	"github.com/charmbracelet/bubbles/viewport" | ||||
|  | @ -67,7 +69,13 @@ func (m uiModel) Init() tea.Cmd { | |||
| 
 | ||||
| func (m *uiModel) updateViewportToSelectedMessage() { | ||||
| 	if message, ok := m.selectedMessage(); ok { | ||||
| 		m.viewport.SetContent(message.Data) | ||||
| 		// TODO: not all messages are JSON
 | ||||
| 		formattedJson := new(bytes.Buffer) | ||||
| 		if err := json.Indent(formattedJson, []byte(message.Data), "", "   "); err == nil { | ||||
| 			m.viewport.SetContent(formattedJson.String()) | ||||
| 		} else { | ||||
| 			m.viewport.SetContent(message.Data) | ||||
| 		} | ||||
| 	} else { | ||||
| 		m.viewport.SetContent("(no message selected)") | ||||
| 	} | ||||
|  | @ -204,8 +212,8 @@ func (m uiModel) headerView() string { | |||
| } | ||||
| 
 | ||||
| func (m uiModel) splitterView() string { | ||||
| 	title := activeHeaderStyle.Render("Message") | ||||
| 	line := activeHeaderStyle.Render(strings.Repeat(" ", max(0, m.viewport.Width-lipgloss.Width(title)))) | ||||
| 	title := inactiveHeaderStyle.Render("Message") | ||||
| 	line := inactiveHeaderStyle.Render(strings.Repeat(" ", max(0, m.viewport.Width-lipgloss.Width(title)))) | ||||
| 	return lipgloss.JoinHorizontal(lipgloss.Left, title, line) | ||||
| } | ||||
| 
 | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue