diff --git a/internal/dynamo-browse/models/itemrender/coll.go b/internal/dynamo-browse/models/itemrender/coll.go new file mode 100644 index 0000000..2277e5b --- /dev/null +++ b/internal/dynamo-browse/models/itemrender/coll.go @@ -0,0 +1,52 @@ +package itemrender + +import ( + "fmt" + "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" + "sort" +) + +type ListRenderer types.AttributeValueMemberL + +func (sr *ListRenderer) TypeName() string { + return "L" +} + +func (sr *ListRenderer) StringValue() string { + if len(sr.Value) == 1 { + return fmt.Sprintf("(1 item)") + } + return fmt.Sprintf("(%d items)", len(sr.Value)) +} + +func (sr *ListRenderer) SubItems() []SubItem { + subitems := make([]SubItem, len(sr.Value)) + for i, r := range sr.Value { + subitems[i] = SubItem{Key: fmt.Sprint(i), Value: ToRenderer(r)} + } + return subitems +} + +type MapRenderer types.AttributeValueMemberM + +func (sr *MapRenderer) TypeName() string { + return "M" +} + +func (sr *MapRenderer) StringValue() string { + if len(sr.Value) == 1 { + return fmt.Sprintf("(1 item)") + } + return fmt.Sprintf("(%d items)", len(sr.Value)) +} + +func (sr *MapRenderer) SubItems() []SubItem { + subitems := make([]SubItem, 0) + for k, r := range sr.Value { + subitems = append(subitems, SubItem{Key: k, Value: ToRenderer(r)}) + } + sort.Slice(subitems, func(i, j int) bool { + return subitems[i].Key < subitems[j].Key + }) + return subitems +} diff --git a/internal/dynamo-browse/models/itemrender/itemdisp.go b/internal/dynamo-browse/models/itemrender/itemdisp.go new file mode 100644 index 0000000..47043c5 --- /dev/null +++ b/internal/dynamo-browse/models/itemrender/itemdisp.go @@ -0,0 +1,49 @@ +package itemrender + +import "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" + +type Renderer interface { + TypeName() string + StringValue() string + SubItems() []SubItem +} + +func ToRenderer(v types.AttributeValue) Renderer { + switch colVal := v.(type) { + case nil: + return nil + case *types.AttributeValueMemberS: + x := StringRenderer(*colVal) + return &x + case *types.AttributeValueMemberN: + x := NumberRenderer(*colVal) + return &x + case *types.AttributeValueMemberBOOL: + x := BoolRenderer(*colVal) + return &x + case *types.AttributeValueMemberNULL: + x := NullRenderer(*colVal) + return &x + case *types.AttributeValueMemberB: + x := BinaryRenderer(*colVal) + return &x + case *types.AttributeValueMemberL: + x := ListRenderer(*colVal) + return &x + case *types.AttributeValueMemberM: + x := MapRenderer(*colVal) + return &x + case *types.AttributeValueMemberBS: + return newBinarySetRenderer(colVal) + case *types.AttributeValueMemberNS: + return newNumberSetRenderer(colVal) + case *types.AttributeValueMemberSS: + return newStringSetRenderer(colVal) + } + return OtherRenderer{} +} + +type SubItem struct { + Key string + Value Renderer +} diff --git a/internal/dynamo-browse/models/itemrender/nils.go b/internal/dynamo-browse/models/itemrender/nils.go new file mode 100644 index 0000000..27a4388 --- /dev/null +++ b/internal/dynamo-browse/models/itemrender/nils.go @@ -0,0 +1,15 @@ +package itemrender + +type OtherRenderer struct{} + +func (u OtherRenderer) TypeName() string { + return "(other)" +} + +func (u OtherRenderer) StringValue() string { + return "(other)" +} + +func (u OtherRenderer) SubItems() []SubItem { + return nil +} diff --git a/internal/dynamo-browse/models/itemrender/scalars.go b/internal/dynamo-browse/models/itemrender/scalars.go new file mode 100644 index 0000000..f9c9b10 --- /dev/null +++ b/internal/dynamo-browse/models/itemrender/scalars.go @@ -0,0 +1,82 @@ +package itemrender + +import ( + "fmt" + "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" +) + +type StringRenderer types.AttributeValueMemberS + +func (sr *StringRenderer) TypeName() string { + return "S" +} + +func (sr *StringRenderer) StringValue() string { + return sr.Value +} + +func (sr *StringRenderer) SubItems() []SubItem { + return nil +} + +type NumberRenderer types.AttributeValueMemberN + +func (sr *NumberRenderer) TypeName() string { + return "N" +} + +func (sr *NumberRenderer) StringValue() string { + return sr.Value +} + +func (sr *NumberRenderer) SubItems() []SubItem { + return nil +} + +type BoolRenderer types.AttributeValueMemberBOOL + +func (sr *BoolRenderer) TypeName() string { + return "BOOL" +} + +func (sr *BoolRenderer) StringValue() string { + if sr.Value { + return "True" + } + return "False" +} + +func (sr *BoolRenderer) SubItems() []SubItem { + return nil +} + +type BinaryRenderer types.AttributeValueMemberB + +func (sr *BinaryRenderer) TypeName() string { + return "B" +} + +func (sr *BinaryRenderer) StringValue() string { + if len(sr.Value) == 1 { + return fmt.Sprintf("(1 byte)") + } + return fmt.Sprintf("(%d bytes)", len(sr.Value)) +} + +func (sr *BinaryRenderer) SubItems() []SubItem { + return nil +} + +type NullRenderer types.AttributeValueMemberNULL + +func (sr *NullRenderer) TypeName() string { + return "NULL" +} + +func (sr *NullRenderer) StringValue() string { + return "null" +} + +func (sr *NullRenderer) SubItems() []SubItem { + return nil +} diff --git a/internal/dynamo-browse/models/itemrender/sets.go b/internal/dynamo-browse/models/itemrender/sets.go new file mode 100644 index 0000000..4885a32 --- /dev/null +++ b/internal/dynamo-browse/models/itemrender/sets.go @@ -0,0 +1,51 @@ +package itemrender + +import ( + "fmt" + "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" +) + +type GenericRenderer struct { + typeName string + subitemValue []Renderer +} + +func (sr *GenericRenderer) TypeName() string { + return sr.typeName +} + +func (sr *GenericRenderer) StringValue() string { + return cardinality(len(sr.subitemValue), "item", "items") +} + +func (sr *GenericRenderer) SubItems() []SubItem { + subitems := make([]SubItem, len(sr.subitemValue)) + for i, r := range sr.subitemValue { + subitems[i] = SubItem{Key: fmt.Sprint(i), Value: r} + } + return subitems +} + +func newBinarySetRenderer(v *types.AttributeValueMemberBS) *GenericRenderer { + vs := make([]Renderer, len(v.Value)) + for i, b := range v.Value { + vs[i] = &BinaryRenderer{Value: b} + } + return &GenericRenderer{typeName: "BS", subitemValue: vs} +} + +func newNumberSetRenderer(v *types.AttributeValueMemberNS) *GenericRenderer { + vs := make([]Renderer, len(v.Value)) + for i, n := range v.Value { + vs[i] = &NumberRenderer{Value: n} + } + return &GenericRenderer{typeName: "NS", subitemValue: vs} +} + +func newStringSetRenderer(v *types.AttributeValueMemberSS) *GenericRenderer { + vs := make([]Renderer, len(v.Value)) + for i, s := range v.Value { + vs[i] = &StringRenderer{Value: s} + } + return &GenericRenderer{typeName: "SS", subitemValue: vs} +} diff --git a/internal/dynamo-browse/models/itemrender/utils.go b/internal/dynamo-browse/models/itemrender/utils.go new file mode 100644 index 0000000..061a636 --- /dev/null +++ b/internal/dynamo-browse/models/itemrender/utils.go @@ -0,0 +1,10 @@ +package itemrender + +import "fmt" + +func cardinality(c int, single, multi string) string { + if c == 1 { + return fmt.Sprintf("(%d %v)", c, single) + } + return fmt.Sprintf("(%d %v)", c, multi) +} diff --git a/internal/dynamo-browse/models/items.go b/internal/dynamo-browse/models/items.go index 49b16dd..d49d0d6 100644 --- a/internal/dynamo-browse/models/items.go +++ b/internal/dynamo-browse/models/items.go @@ -1,6 +1,9 @@ package models -import "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" +import ( + "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" + "github.com/lmika/awstools/internal/dynamo-browse/models/itemrender" +) type Item map[string]types.AttributeValue @@ -28,3 +31,7 @@ func (i Item) KeyValue(info *TableInfo) map[string]types.AttributeValue { func (i Item) AttributeValueAsString(key string) (string, bool) { return attributeToString(i[key]) } + +func (i Item) Renderer(key string) itemrender.Renderer { + return itemrender.ToRenderer(i[key]) +} diff --git a/internal/dynamo-browse/ui/teamodels/dynamoitemview/model.go b/internal/dynamo-browse/ui/teamodels/dynamoitemview/model.go index 9d874dc..4109f7b 100644 --- a/internal/dynamo-browse/ui/teamodels/dynamoitemview/model.go +++ b/internal/dynamo-browse/ui/teamodels/dynamoitemview/model.go @@ -2,10 +2,11 @@ package dynamoitemview import ( "fmt" + "github.com/lmika/awstools/internal/dynamo-browse/models/itemrender" + "io" "strings" "text/tabwriter" - "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" @@ -83,15 +84,8 @@ func (m *Model) updateViewportToSelectedMessage() { viewportContent := &strings.Builder{} tabWriter := tabwriter.NewWriter(viewportContent, 0, 1, 1, ' ', 0) for _, colName := range m.currentResultSet.Columns { - switch colVal := m.selectedItem[colName].(type) { - case nil: - break - case *types.AttributeValueMemberS: - fmt.Fprintf(tabWriter, "%v\tS\t%s\n", colName, colVal.Value) - case *types.AttributeValueMemberN: - fmt.Fprintf(tabWriter, "%v\tN\t%s\n", colName, colVal.Value) - default: - fmt.Fprintf(tabWriter, "%v\t?\t%s\n", colName, "(other)") + if r := m.selectedItem.Renderer(colName); r != nil { + m.renderItem(tabWriter, "", colName, r) } } @@ -100,3 +94,12 @@ func (m *Model) updateViewportToSelectedMessage() { m.viewport.Height = m.h - m.frameTitle.HeaderHeight() m.viewport.SetContent(viewportContent.String()) } + +func (m *Model) renderItem(w io.Writer, prefix string, name string, r itemrender.Renderer) { + fmt.Fprintf(w, "%s%v\t%s\t%s\n", prefix, name, r.TypeName(), r.StringValue()) + if subitems := r.SubItems(); len(subitems) > 0 { + for _, si := range subitems { + m.renderItem(w, prefix+" ", si.Key, si.Value) + } + } +} diff --git a/internal/dynamo-browse/ui/teamodels/dynamotableview/tblmodel.go b/internal/dynamo-browse/ui/teamodels/dynamotableview/tblmodel.go index 65e8dd3..e3a726e 100644 --- a/internal/dynamo-browse/ui/teamodels/dynamotableview/tblmodel.go +++ b/internal/dynamo-browse/ui/teamodels/dynamotableview/tblmodel.go @@ -6,18 +6,17 @@ import ( "io" "strings" - "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" table "github.com/calyptia/go-bubble-table" "github.com/lmika/awstools/internal/dynamo-browse/models" ) var ( markedRowStyle = lipgloss.NewStyle(). - Background(lipgloss.Color("#e1e1e1")) + Background(lipgloss.Color("#e1e1e1")) dirtyRowStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color("#e13131")) + Foreground(lipgloss.Color("#e13131")) newRowStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color("#31e131")) + Foreground(lipgloss.Color("#31e131")) ) type itemTableRow struct { @@ -37,15 +36,8 @@ func (mtr itemTableRow) Render(w io.Writer, model table.Model, index int) { sb.WriteString("\t") } - switch colVal := mtr.item[colName].(type) { - case nil: - sb.WriteString("(nil)") - case *types.AttributeValueMemberS: - sb.WriteString(colVal.Value) - case *types.AttributeValueMemberN: - sb.WriteString(colVal.Value) - default: - sb.WriteString("(other)") + if r := mtr.item.Renderer(colName); r != nil { + sb.WriteString(r.StringValue()) } } diff --git a/test/cmd/load-test-table/main.go b/test/cmd/load-test-table/main.go index df59068..34a5544 100644 --- a/test/cmd/load-test-table/main.go +++ b/test/cmd/load-test-table/main.go @@ -5,6 +5,7 @@ import ( "github.com/brianvoe/gofakeit/v6" "github.com/google/uuid" "log" + "strconv" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/config" @@ -66,13 +67,22 @@ func main() { for i := 0; i < totalItems; i++ { key := uuid.New().String() if err := tableService.Put(ctx, tableInfo, models.Item{ - "pk": &types.AttributeValueMemberS{Value: key}, - "sk": &types.AttributeValueMemberS{Value: key}, - "name": &types.AttributeValueMemberS{Value: gofakeit.Name()}, - "address": &types.AttributeValueMemberS{Value: gofakeit.Address().Address}, - "city": &types.AttributeValueMemberS{Value: gofakeit.Address().City}, - "phone": &types.AttributeValueMemberS{Value: gofakeit.Phone()}, - "web": &types.AttributeValueMemberS{Value: gofakeit.URL()}, + "pk": &types.AttributeValueMemberS{Value: key}, + "sk": &types.AttributeValueMemberS{Value: key}, + "name": &types.AttributeValueMemberS{Value: gofakeit.Name()}, + "address": &types.AttributeValueMemberS{Value: gofakeit.Address().Address}, + "city": &types.AttributeValueMemberS{Value: gofakeit.Address().City}, + "phone": &types.AttributeValueMemberN{Value: gofakeit.Phone()}, + "web": &types.AttributeValueMemberS{Value: gofakeit.URL()}, + "inOffice": &types.AttributeValueMemberBOOL{Value: gofakeit.Bool()}, + "ratings": &types.AttributeValueMemberL{Value: []types.AttributeValue{ + &types.AttributeValueMemberS{Value: gofakeit.Adverb()}, + &types.AttributeValueMemberN{Value: strconv.Itoa(int(gofakeit.Int32()))}, + }}, + "values": &types.AttributeValueMemberM{Value: map[string]types.AttributeValue{ + "adverb": &types.AttributeValueMemberS{Value: gofakeit.Adverb()}, + "int": &types.AttributeValueMemberN{Value: strconv.Itoa(int(gofakeit.Int32()))}, + }}, }); err != nil { log.Fatalln(err) }