Merged all 'set-X' commands into a single 'set-attr' command

This commit is contained in:
Leon Mika 2022-07-16 10:05:48 +10:00
parent 716adbdce5
commit 9fee17a6a6
9 changed files with 317 additions and 43 deletions

View file

@ -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
}
}

View file

@ -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
//}

View file

@ -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.",

View file

@ -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

View file

@ -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))
})
}

View file

@ -0,0 +1,11 @@
package models
type ItemType string
const (
UnsetItemType ItemType = ""
StringItemType ItemType = "S"
NumberItemType ItemType = "N"
BoolItemType ItemType = "BOOL"
NullItemType ItemType = "NULL"
)

View file

@ -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"),
},
})

View file

@ -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 {

View file

@ -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{