ucl: Have started adding some of the psudo variables

This commit is contained in:
Leon Mika 2025-05-18 13:42:44 +10:00
parent 18ffe85a56
commit 40f8dd76e2
10 changed files with 267 additions and 19 deletions

2
go.mod
View file

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

10
go.sum
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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