From 9fee17a6a66099b6f59af39b50e394f18e6d8e11 Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Sat, 16 Jul 2022 10:05:48 +1000 Subject: [PATCH] Merged all 'set-X' commands into a single 'set-attr' command --- internal/common/ui/commandctrl/commandctrl.go | 16 +- .../dynamo-browse/controllers/commands.go | 19 +++ .../controllers/tableread_test.go | 9 +- .../dynamo-browse/controllers/tablewrite.go | 109 +++++++++--- .../controllers/tablewrite_test.go | 157 +++++++++++++++++- internal/dynamo-browse/models/itemtype.go | 11 ++ internal/dynamo-browse/ui/model.go | 35 +++- test/cmd/load-test-table/main.go | 2 +- test/testdynamo/client.go | 2 +- 9 files changed, 317 insertions(+), 43 deletions(-) create mode 100644 internal/dynamo-browse/models/itemtype.go diff --git a/internal/common/ui/commandctrl/commandctrl.go b/internal/common/ui/commandctrl/commandctrl.go index a71352a..c13036c 100644 --- a/internal/common/ui/commandctrl/commandctrl.go +++ b/internal/common/ui/commandctrl/commandctrl.go @@ -37,14 +37,12 @@ func (c *CommandController) Prompt() tea.Cmd { } func (c *CommandController) Execute(commandInput string) tea.Cmd { - log.Println("Received input: ", commandInput) input := strings.TrimSpace(commandInput) if input == "" { return nil } tokens := shellwords.Split(input) - log.Println("Tokens: ", tokens) command := c.lookupCommand(tokens[0]) if command == nil { log.Println("No such command: ", tokens) @@ -54,6 +52,18 @@ func (c *CommandController) Execute(commandInput string) tea.Cmd { return command(tokens[1:]) } +func (c *CommandController) Alias(commandName string) Command { + return func(args []string) tea.Cmd { + command := c.lookupCommand(commandName) + if command == nil { + log.Println("No such command: ", commandName) + return events.SetError(errors.New("no such command: " + commandName)) + } + + return command(args[1:]) + } +} + func (c *CommandController) lookupCommand(name string) Command { for ctx := c.commandList; ctx != nil; ctx = ctx.parent { log.Printf("Looking in command list: %v", c.commandList) @@ -62,4 +72,4 @@ func (c *CommandController) lookupCommand(name string) Command { } } return nil -} \ No newline at end of file +} diff --git a/internal/dynamo-browse/controllers/commands.go b/internal/dynamo-browse/controllers/commands.go index 5b7c630..82d242a 100644 --- a/internal/dynamo-browse/controllers/commands.go +++ b/internal/dynamo-browse/controllers/commands.go @@ -23,3 +23,22 @@ func (ps *promptSequence) next() tea.Msg { } return ps.onAllDone(ps.receivedValues) } + +//type SetAttributeArg struct { +// attrType models.ItemType +// attrName string +//} +// +//func ParseSetAttributeArgs(args []string) (attrArgs []SetAttributeArg, err error) { +// var currArg SetAttributeArg +// for _, arg := range args { +// if arg[0] == '-' { +// currArg.attrType = models.ItemType(arg[1:]) +// } else { +// currArg.attrName = arg +// attrArgs = append(attrArgs, currArg) +// currArg = SetAttributeArg{} +// } +// } +// return attrArgs, nil +//} diff --git a/internal/dynamo-browse/controllers/tableread_test.go b/internal/dynamo-browse/controllers/tableread_test.go index 803c4bb..d4ee054 100644 --- a/internal/dynamo-browse/controllers/tableread_test.go +++ b/internal/dynamo-browse/controllers/tableread_test.go @@ -209,10 +209,11 @@ var testData = []testdynamo.TestData{ TableName: "alpha-table", Data: []map[string]interface{}{ { - "pk": "abc", - "sk": "111", - "alpha": "This is some value", - "age": 23, + "pk": "abc", + "sk": "111", + "alpha": "This is some value", + "age": 23, + "useMailing": true, "address": map[string]any{ "no": 123, "street": "Fake st.", diff --git a/internal/dynamo-browse/controllers/tablewrite.go b/internal/dynamo-browse/controllers/tablewrite.go index a6b2f2f..87b4c06 100644 --- a/internal/dynamo-browse/controllers/tablewrite.go +++ b/internal/dynamo-browse/controllers/tablewrite.go @@ -9,6 +9,7 @@ import ( "github.com/lmika/awstools/internal/dynamo-browse/models" "github.com/lmika/awstools/internal/dynamo-browse/services/tables" "github.com/pkg/errors" + "strconv" ) type TableWriteController struct { @@ -68,24 +69,50 @@ func (twc *TableWriteController) NewItem() tea.Cmd { } } -func (twc *TableWriteController) SetStringValue(idx int, key string) tea.Cmd { - return func() tea.Msg { - // Verify that the expression is valid - apPath := newAttrPath(key) +func (twc *TableWriteController) SetAttributeValue(idx int, itemType models.ItemType, key string) tea.Cmd { + apPath := newAttrPath(key) - if err := twc.state.withResultSetReturningError(func(set *models.ResultSet) error { - _, err := apPath.follow(set.Items()[idx]) - return err - }); err != nil { - return events.Error(err) + var attrValue types.AttributeValue + if err := twc.state.withResultSetReturningError(func(set *models.ResultSet) (err error) { + attrValue, err = apPath.follow(set.Items()[idx]) + return err + }); err != nil { + return events.SetError(err) + } + + switch itemType { + case models.UnsetItemType: + switch attrValue.(type) { + case *types.AttributeValueMemberS: + return twc.setStringValue(idx, apPath) + case *types.AttributeValueMemberN: + return twc.setNumberValue(idx, apPath) + case *types.AttributeValueMemberBOOL: + return twc.setBoolValue(idx, apPath) + default: + return events.SetError(errors.New("attribute type for key must be set")) } + case models.StringItemType: + return twc.setStringValue(idx, apPath) + case models.NumberItemType: + return twc.setNumberValue(idx, apPath) + case models.BoolItemType: + return twc.setBoolValue(idx, apPath) + case models.NullItemType: + return twc.setNullValue(idx, apPath) + default: + return events.SetError(errors.New("unsupported attribute type")) + } +} +func (twc *TableWriteController) setStringValue(idx int, attr attrPath) tea.Cmd { + return func() tea.Msg { return events.PromptForInputMsg{ Prompt: "string value: ", OnDone: func(value string) tea.Cmd { return func() tea.Msg { if err := twc.state.withResultSetReturningError(func(set *models.ResultSet) error { - err := apPath.setAt(set.Items()[idx], &types.AttributeValueMemberS{Value: value}) + err := attr.setAt(set.Items()[idx], &types.AttributeValueMemberS{Value: value}) if err != nil { return err } @@ -103,24 +130,14 @@ func (twc *TableWriteController) SetStringValue(idx int, key string) tea.Cmd { } } -func (twc *TableWriteController) SetNumberValue(idx int, key string) tea.Cmd { +func (twc *TableWriteController) setNumberValue(idx int, attr attrPath) tea.Cmd { return func() tea.Msg { - // Verify that the expression is valid - apPath := newAttrPath(key) - - if err := twc.state.withResultSetReturningError(func(set *models.ResultSet) error { - _, err := apPath.follow(set.Items()[idx]) - return err - }); err != nil { - return events.Error(err) - } - return events.PromptForInputMsg{ Prompt: "number value: ", OnDone: func(value string) tea.Cmd { return func() tea.Msg { if err := twc.state.withResultSetReturningError(func(set *models.ResultSet) error { - err := apPath.setAt(set.Items()[idx], &types.AttributeValueMemberN{Value: value}) + err := attr.setAt(set.Items()[idx], &types.AttributeValueMemberN{Value: value}) if err != nil { return err } @@ -138,6 +155,54 @@ func (twc *TableWriteController) SetNumberValue(idx int, key string) tea.Cmd { } } +func (twc *TableWriteController) setBoolValue(idx int, attr attrPath) tea.Cmd { + return func() tea.Msg { + return events.PromptForInputMsg{ + Prompt: "bool value: ", + OnDone: func(value string) tea.Cmd { + return func() tea.Msg { + b, err := strconv.ParseBool(value) + if err != nil { + return events.Error(err) + } + + if err := twc.state.withResultSetReturningError(func(set *models.ResultSet) error { + err := attr.setAt(set.Items()[idx], &types.AttributeValueMemberBOOL{Value: b}) + if err != nil { + return err + } + + set.SetDirty(idx, true) + set.RefreshColumns() + return nil + }); err != nil { + return events.Error(err) + } + return ResultSetUpdated{} + } + }, + } + } +} + +func (twc *TableWriteController) setNullValue(idx int, attr attrPath) tea.Cmd { + return func() tea.Msg { + if err := twc.state.withResultSetReturningError(func(set *models.ResultSet) error { + err := attr.setAt(set.Items()[idx], &types.AttributeValueMemberNULL{Value: true}) + if err != nil { + return err + } + + set.SetDirty(idx, true) + set.RefreshColumns() + return nil + }); err != nil { + return events.Error(err) + } + return ResultSetUpdated{} + } +} + func (twc *TableWriteController) DeleteAttribute(idx int, key string) tea.Cmd { return func() tea.Msg { // Verify that the expression is valid diff --git a/internal/dynamo-browse/controllers/tablewrite_test.go b/internal/dynamo-browse/controllers/tablewrite_test.go index 98c593e..020ef7a 100644 --- a/internal/dynamo-browse/controllers/tablewrite_test.go +++ b/internal/dynamo-browse/controllers/tablewrite_test.go @@ -1,8 +1,10 @@ package controllers_test import ( + "fmt" "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" "github.com/lmika/awstools/internal/dynamo-browse/controllers" + "github.com/lmika/awstools/internal/dynamo-browse/models" "github.com/lmika/awstools/internal/dynamo-browse/providers/dynamo" "github.com/lmika/awstools/internal/dynamo-browse/services/tables" "github.com/lmika/awstools/test/testdynamo" @@ -41,6 +43,152 @@ func TestTableWriteController_NewItem(t *testing.T) { }) } +func TestTableWriteController_SetAttributeValue(t *testing.T) { + t.Run("should preserve the type of the field if unspecified", func(t *testing.T) { + client, cleanupFn := testdynamo.SetupTestTable(t, testData) + t.Cleanup(cleanupFn) + + provider := dynamo.NewProvider(client) + service := tables.NewService(provider) + + scenarios := []struct { + attrKey string + attrValue string + expected types.AttributeValue + }{ + { + attrKey: "alpha", + attrValue: "a new value", + expected: &types.AttributeValueMemberS{Value: "a new value"}, + }, + { + attrKey: "age", + attrValue: "1234", + expected: &types.AttributeValueMemberN{Value: "1234"}, + }, + { + attrKey: "useMailing", + attrValue: "t", + expected: &types.AttributeValueMemberBOOL{Value: true}, + }, + { + attrKey: "useMailing", + attrValue: "f", + expected: &types.AttributeValueMemberBOOL{Value: false}, + }, + } + + for _, scenario := range scenarios { + t.Run(fmt.Sprintf("should set value of field %v", scenario.attrKey), func(t *testing.T) { + state := controllers.NewState() + readController := controllers.NewTableReadController(state, service, "alpha-table") + writeController := controllers.NewTableWriteController(state, service, readController) + + invokeCommand(t, readController.Init()) + before, _ := state.ResultSet().Items()[0].AttributeValueAsString("alpha") + assert.Equal(t, "This is some value", before) + assert.False(t, state.ResultSet().IsDirty(0)) + + invokeCommandWithPrompt(t, writeController.SetAttributeValue(0, models.UnsetItemType, scenario.attrKey), scenario.attrValue) + + after, _ := state.ResultSet().Items()[0][scenario.attrKey] + assert.Equal(t, scenario.expected, after) + assert.True(t, state.ResultSet().IsDirty(0)) + }) + } + }) + + t.Run("should change the value to a particular field if already present", func(t *testing.T) { + client, cleanupFn := testdynamo.SetupTestTable(t, testData) + t.Cleanup(cleanupFn) + + provider := dynamo.NewProvider(client) + service := tables.NewService(provider) + + scenarios := []struct { + attrType models.ItemType + attrValue string + expected types.AttributeValue + }{ + { + attrType: models.StringItemType, + attrValue: "a new value", + expected: &types.AttributeValueMemberS{Value: "a new value"}, + }, + { + attrType: models.NumberItemType, + attrValue: "1234", + expected: &types.AttributeValueMemberN{Value: "1234"}, + }, + { + attrType: models.BoolItemType, + attrValue: "true", + expected: &types.AttributeValueMemberBOOL{Value: true}, + }, + { + attrType: models.BoolItemType, + attrValue: "false", + expected: &types.AttributeValueMemberBOOL{Value: false}, + }, + { + attrType: models.NullItemType, + attrValue: "", + expected: &types.AttributeValueMemberNULL{Value: true}, + }, + } + + for _, scenario := range scenarios { + t.Run(fmt.Sprintf("should change the value of a field to type %v", scenario.attrType), func(t *testing.T) { + state := controllers.NewState() + readController := controllers.NewTableReadController(state, service, "alpha-table") + writeController := controllers.NewTableWriteController(state, service, readController) + + invokeCommand(t, readController.Init()) + before, _ := state.ResultSet().Items()[0].AttributeValueAsString("alpha") + assert.Equal(t, "This is some value", before) + assert.False(t, state.ResultSet().IsDirty(0)) + + if scenario.attrValue == "" { + invokeCommand(t, writeController.SetAttributeValue(0, scenario.attrType, "alpha")) + } else { + invokeCommandWithPrompt(t, writeController.SetAttributeValue(0, scenario.attrType, "alpha"), scenario.attrValue) + } + + after, _ := state.ResultSet().Items()[0]["alpha"] + assert.Equal(t, scenario.expected, after) + assert.True(t, state.ResultSet().IsDirty(0)) + }) + + t.Run(fmt.Sprintf("should change value of nested field to type %v", scenario.attrType), func(t *testing.T) { + state := controllers.NewState() + readController := controllers.NewTableReadController(state, service, "alpha-table") + writeController := controllers.NewTableWriteController(state, service, readController) + + invokeCommand(t, readController.Init()) + + beforeAddress := state.ResultSet().Items()[0]["address"].(*types.AttributeValueMemberM) + beforeStreet := beforeAddress.Value["street"].(*types.AttributeValueMemberS).Value + + assert.Equal(t, "Fake st.", beforeStreet) + assert.False(t, state.ResultSet().IsDirty(0)) + + if scenario.attrValue == "" { + invokeCommand(t, writeController.SetAttributeValue(0, scenario.attrType, "address.street")) + } else { + invokeCommandWithPrompt(t, writeController.SetAttributeValue(0, scenario.attrType, "address.street"), scenario.attrValue) + } + + afterAddress := state.ResultSet().Items()[0]["address"].(*types.AttributeValueMemberM) + after := afterAddress.Value["street"] + + assert.Equal(t, scenario.expected, after) + assert.True(t, state.ResultSet().IsDirty(0)) + }) + } + }) +} + +/* func TestTableWriteController_SetStringValue(t *testing.T) { client, cleanupFn := testdynamo.SetupTestTable(t, testData) defer cleanupFn() @@ -134,6 +282,7 @@ func TestTableWriteController_SetNumberValue(t *testing.T) { assert.True(t, state.ResultSet().IsDirty(0)) }) } +*/ func TestTableWriteController_DeleteAttribute(t *testing.T) { client, cleanupFn := testdynamo.SetupTestTable(t, testData) @@ -199,7 +348,7 @@ func TestTableWriteController_PutItem(t *testing.T) { assert.False(t, state.ResultSet().IsDirty(0)) // Modify the item and put it - invokeCommandWithPrompt(t, writeController.SetStringValue(0, "alpha"), "a new value") + invokeCommandWithPrompt(t, writeController.SetAttributeValue(0, models.StringItemType, "alpha"), "a new value") invokeCommandWithPrompt(t, writeController.PutItem(0), "y") // Rescan the table @@ -227,7 +376,7 @@ func TestTableWriteController_PutItem(t *testing.T) { assert.False(t, state.ResultSet().IsDirty(0)) // Modify the item but do not put it - invokeCommandWithPrompt(t, writeController.SetStringValue(0, "alpha"), "a new value") + invokeCommandWithPrompt(t, writeController.SetAttributeValue(0, models.StringItemType, "alpha"), "a new value") invokeCommandWithPrompt(t, writeController.PutItem(0), "n") current, _ := state.ResultSet().Items()[0].AttributeValueAsString("alpha") @@ -308,7 +457,7 @@ func TestTableWriteController_TouchItem(t *testing.T) { assert.False(t, state.ResultSet().IsDirty(0)) // Modify the item and put it - invokeCommandWithPrompt(t, writeController.SetStringValue(0, "alpha"), "a new value") + invokeCommandWithPrompt(t, writeController.SetAttributeValue(0, models.StringItemType, "alpha"), "a new value") invokeCommandExpectingError(t, writeController.TouchItem(0)) }) } @@ -359,7 +508,7 @@ func TestTableWriteController_NoisyTouchItem(t *testing.T) { assert.False(t, state.ResultSet().IsDirty(0)) // Modify the item and put it - invokeCommandWithPrompt(t, writeController.SetStringValue(0, "alpha"), "a new value") + invokeCommandWithPrompt(t, writeController.SetAttributeValue(0, models.StringItemType, "alpha"), "a new value") invokeCommandExpectingError(t, writeController.NoisyTouchItem(0)) }) } diff --git a/internal/dynamo-browse/models/itemtype.go b/internal/dynamo-browse/models/itemtype.go new file mode 100644 index 0000000..4174005 --- /dev/null +++ b/internal/dynamo-browse/models/itemtype.go @@ -0,0 +1,11 @@ +package models + +type ItemType string + +const ( + UnsetItemType ItemType = "" + StringItemType ItemType = "S" + NumberItemType ItemType = "N" + BoolItemType ItemType = "BOOL" + NullItemType ItemType = "NULL" +) diff --git a/internal/dynamo-browse/ui/model.go b/internal/dynamo-browse/ui/model.go index 83ccb45..ba558ec 100644 --- a/internal/dynamo-browse/ui/model.go +++ b/internal/dynamo-browse/ui/model.go @@ -5,8 +5,9 @@ import ( "github.com/lmika/awstools/internal/common/ui/commandctrl" "github.com/lmika/awstools/internal/common/ui/events" "github.com/lmika/awstools/internal/dynamo-browse/controllers" - "github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/dynamoitemedit" + "github.com/lmika/awstools/internal/dynamo-browse/models" "github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/dialogprompt" + "github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/dynamoitemedit" "github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/dynamoitemview" "github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/dynamotableview" "github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/layout" @@ -14,6 +15,7 @@ import ( "github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/styles" "github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/tableselect" "github.com/pkg/errors" + "strings" ) type Model struct { @@ -61,17 +63,29 @@ func NewModel(rc *controllers.TableReadController, wc *controllers.TableWriteCon // TEMP "new-item": commandctrl.NoArgCommand(wc.NewItem()), - "set-s": func(args []string) tea.Cmd { + "set-attr": func(args []string) tea.Cmd { if len(args) == 0 { return events.SetError(errors.New("expected field")) } - return wc.SetStringValue(dtv.SelectedItemIndex(), args[0]) - }, - "set-n": func(args []string) tea.Cmd { - if len(args) == 0 { - return events.SetError(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 + default: + return events.SetError(errors.New("unrecognised item type")) + } + args = args[1:] } - return wc.SetNumberValue(dtv.SelectedItemIndex(), args[0]) + + return wc.SetAttributeValue(dtv.SelectedItemIndex(), itemType, args[0]) }, "del-attr": func(args []string) tea.Cmd { if len(args) == 0 { @@ -89,6 +103,11 @@ func NewModel(rc *controllers.TableReadController, wc *controllers.TableWriteCon "noisy-touch": func(args []string) tea.Cmd { return wc.NoisyTouchItem(dtv.SelectedItemIndex()) }, + + // Aliases + "sa": cc.Alias("set-attr"), + "da": cc.Alias("del-attr"), + "w": cc.Alias("put"), }, }) diff --git a/test/cmd/load-test-table/main.go b/test/cmd/load-test-table/main.go index 44efdde..db00c13 100644 --- a/test/cmd/load-test-table/main.go +++ b/test/cmd/load-test-table/main.go @@ -19,7 +19,7 @@ import ( func main() { ctx := context.Background() tableName := "business-addresses" - totalItems := 5000 + totalItems := 500 cfg, err := config.LoadDefaultConfig(ctx) if err != nil { diff --git a/test/testdynamo/client.go b/test/testdynamo/client.go index bc23d23..1890af9 100644 --- a/test/testdynamo/client.go +++ b/test/testdynamo/client.go @@ -28,7 +28,7 @@ func SetupTestTable(t *testing.T, testData []TestData) (*dynamodb.Client, func() assert.NoError(t, err) dynamoClient := dynamodb.NewFromConfig(cfg, - dynamodb.WithEndpointResolver(dynamodb.EndpointResolverFromURL("http://localhost:18000"))) + dynamodb.WithEndpointResolver(dynamodb.EndpointResolverFromURL("http://localhost:4566"))) for _, table := range testData { _, err = dynamoClient.CreateTable(ctx, &dynamodb.CreateTableInput{