Made table information available to scripts (#49)
- Added a property with table information to session and result set Script types - Added the ability to add new key bindings to the script - Rebuilt the foreground job dispatcher to reduce the occurrence of the progress indicator showing up when no job was running. - Fixed rebinding of keys. Rebinding a key will no longer clear other keys for the old or new bindings.
This commit is contained in:
parent
733e59ec95
commit
3f1aec2c87
|
@ -114,7 +114,7 @@ func main() {
|
||||||
scriptController := controllers.NewScriptController(scriptManagerService, tableReadController, settingsController, eventBus)
|
scriptController := controllers.NewScriptController(scriptManagerService, tableReadController, settingsController, eventBus)
|
||||||
|
|
||||||
keyBindingService := keybindings_service.NewService(keyBindings)
|
keyBindingService := keybindings_service.NewService(keyBindings)
|
||||||
keyBindingController := controllers.NewKeyBindingController(keyBindingService)
|
keyBindingController := controllers.NewKeyBindingController(keyBindingService, scriptController)
|
||||||
|
|
||||||
commandController := commandctrl.NewCommandController(inputHistoryService)
|
commandController := commandctrl.NewCommandController(inputHistoryService)
|
||||||
commandController.AddCommandLookupExtension(scriptController)
|
commandController.AddCommandLookupExtension(scriptController)
|
||||||
|
|
|
@ -3,6 +3,7 @@ package controllers
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
"github.com/lmika/audax/internal/dynamo-browse/models"
|
"github.com/lmika/audax/internal/dynamo-browse/models"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
)
|
)
|
||||||
|
@ -25,3 +26,10 @@ type SettingsProvider interface {
|
||||||
SetScriptLookupPaths(value string) error
|
SetScriptLookupPaths(value string) error
|
||||||
ScriptLookupPaths() string
|
ScriptLookupPaths() string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CustomKeyBindingSource interface {
|
||||||
|
LookupBinding(theKey string) string
|
||||||
|
CustomKeyCommand(key string) tea.Cmd
|
||||||
|
UnbindKey(key string)
|
||||||
|
Rebind(bindingName string, newKey string) error
|
||||||
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
"github.com/lmika/audax/internal/common/ui/events"
|
"github.com/lmika/audax/internal/common/ui/events"
|
||||||
|
"github.com/lmika/audax/internal/dynamo-browse/services/jobs"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewJob[T any](jc *JobsController, description string, job func(ctx context.Context) (T, error)) JobBuilder[T] {
|
func NewJob[T any](jc *JobsController, description string, job func(ctx context.Context) (T, error)) JobBuilder[T] {
|
||||||
|
@ -61,7 +62,7 @@ func (jb JobBuilder[T]) executeJob(ctx context.Context) tea.Msg {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (jb JobBuilder[T]) doSubmit() tea.Msg {
|
func (jb JobBuilder[T]) doSubmit() tea.Msg {
|
||||||
jb.jc.service.SubmitForegroundJob(func(ctx context.Context) {
|
if err := jb.jc.service.SubmitForegroundJob(jobs.WithDescription(jb.description, jobs.JobFunc(func(ctx context.Context) {
|
||||||
msg := jb.executeJob(ctx)
|
msg := jb.executeJob(ctx)
|
||||||
|
|
||||||
jb.jc.msgSender(msg)
|
jb.jc.msgSender(msg)
|
||||||
|
@ -73,12 +74,9 @@ func (jb JobBuilder[T]) doSubmit() tea.Msg {
|
||||||
JobStatus: "",
|
JobStatus: "",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}, func(msg string) {
|
}))); err != nil {
|
||||||
jb.jc.msgSender(events.ForegroundJobUpdate{
|
return events.Error(err)
|
||||||
JobRunning: true,
|
}
|
||||||
JobStatus: jb.description + " " + msg,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
return events.ForegroundJobUpdate{
|
return events.ForegroundJobUpdate{
|
||||||
JobRunning: true,
|
JobRunning: true,
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"github.com/lmika/audax/internal/common/ui/events"
|
"github.com/lmika/audax/internal/common/ui/events"
|
||||||
"github.com/lmika/audax/internal/dynamo-browse/services/jobs"
|
"github.com/lmika/audax/internal/dynamo-browse/services/jobs"
|
||||||
bus "github.com/lmika/events"
|
bus "github.com/lmika/events"
|
||||||
|
"log"
|
||||||
)
|
)
|
||||||
|
|
||||||
type JobsController struct {
|
type JobsController struct {
|
||||||
|
@ -18,6 +19,9 @@ func NewJobsController(service *jobs.Services, bus *bus.Bus, immediate bool) *Jo
|
||||||
service: service,
|
service: service,
|
||||||
immediate: immediate,
|
immediate: immediate,
|
||||||
}
|
}
|
||||||
|
bus.On(jobs.JobStartEvent, func(job jobs.EventData) { jc.sendForegroundJobState(job.Job, "") })
|
||||||
|
bus.On(jobs.JobIdleEvent, func() { jc.sendForegroundJobState(nil, "") })
|
||||||
|
bus.On(jobs.JobUpdateEvent, func(job jobs.EventData, update string) { jc.sendForegroundJobState(job.Job, update) })
|
||||||
|
|
||||||
return jc
|
return jc
|
||||||
}
|
}
|
||||||
|
@ -36,3 +40,30 @@ func (js *JobsController) CancelRunningJob(ifNoJobsRunning func() tea.Msg) tea.M
|
||||||
}
|
}
|
||||||
return ifNoJobsRunning()
|
return ifNoJobsRunning()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (jc *JobsController) sendForegroundJobState(job jobs.Job, update string) {
|
||||||
|
if job == nil {
|
||||||
|
log.Printf("job service idle")
|
||||||
|
jc.msgSender(events.ForegroundJobUpdate{
|
||||||
|
JobRunning: false,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var statusMessage string
|
||||||
|
if dj, ok := job.(jobs.DescribableJob); ok {
|
||||||
|
statusMessage = dj.Description
|
||||||
|
} else {
|
||||||
|
statusMessage = "Working…"
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(update) > 0 {
|
||||||
|
statusMessage += " " + update
|
||||||
|
}
|
||||||
|
log.Printf("job update: %v", statusMessage)
|
||||||
|
|
||||||
|
jc.msgSender(events.ForegroundJobUpdate{
|
||||||
|
JobRunning: true,
|
||||||
|
JobStatus: statusMessage,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -9,32 +9,80 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type KeyBindingController struct {
|
type KeyBindingController struct {
|
||||||
service *keybindings.Service
|
service *keybindings.Service
|
||||||
|
customBindingSource CustomKeyBindingSource
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewKeyBindingController(service *keybindings.Service) *KeyBindingController {
|
func NewKeyBindingController(service *keybindings.Service, customBindingSource CustomKeyBindingSource) *KeyBindingController {
|
||||||
return &KeyBindingController{service: service}
|
return &KeyBindingController{
|
||||||
|
service: service,
|
||||||
|
customBindingSource: customBindingSource,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (kb *KeyBindingController) Rebind(bindingName string, newKey string, force bool) tea.Msg {
|
func (kb *KeyBindingController) Rebind(bindingName string, newKey string, force bool) tea.Msg {
|
||||||
err := kb.service.Rebind(bindingName, newKey, force)
|
existingBinding := kb.findExistingBinding(newKey)
|
||||||
if err == nil {
|
if existingBinding == "" {
|
||||||
|
if err := kb.rebind(bindingName, newKey); err != nil {
|
||||||
|
return events.Error(err)
|
||||||
|
}
|
||||||
return events.StatusMsg(fmt.Sprintf("Binding '%v' now bound to '%v'", bindingName, newKey))
|
return events.StatusMsg(fmt.Sprintf("Binding '%v' now bound to '%v'", bindingName, newKey))
|
||||||
} else if force {
|
|
||||||
return events.Error(errors.Wrapf(err, "cannot bind '%v' to '%v'", bindingName, newKey))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var keyAlreadyBoundErr keybindings.KeyAlreadyBoundError
|
//err := kb.rebind(bindingName, newKey, force)
|
||||||
if errors.As(err, &keyAlreadyBoundErr) {
|
//if err == nil {
|
||||||
promptMsg := fmt.Sprintf("Key '%v' already bound to '%v'. Continue? ", keyAlreadyBoundErr.Key, keyAlreadyBoundErr.ExistingBindingName)
|
// return events.StatusMsg(fmt.Sprintf("Binding '%v' now bound to '%v'", bindingName, newKey))
|
||||||
return events.ConfirmYes(promptMsg, func() tea.Msg {
|
//} else if force {
|
||||||
err := kb.service.Rebind(bindingName, newKey, true)
|
// return events.Error(errors.Wrapf(err, "cannot bind '%v' to '%v'", bindingName, newKey))
|
||||||
if err != nil {
|
//}
|
||||||
return events.Error(err)
|
//
|
||||||
}
|
//var keyAlreadyBoundErr keybindings.KeyAlreadyBoundError
|
||||||
return events.StatusMsg(fmt.Sprintf("Binding '%v' now bound to '%v'", bindingName, newKey))
|
//if errors.As(err, &keyAlreadyBoundErr) {
|
||||||
})
|
promptMsg := fmt.Sprintf("Key '%v' already bound to '%v'. Continue? ", newKey, existingBinding)
|
||||||
}
|
return events.ConfirmYes(promptMsg, func() tea.Msg {
|
||||||
|
kb.unbindKey(newKey)
|
||||||
|
|
||||||
return events.Error(err)
|
err := kb.rebind(bindingName, newKey)
|
||||||
|
if err != nil {
|
||||||
|
return events.Error(err)
|
||||||
|
}
|
||||||
|
return events.StatusMsg(fmt.Sprintf("Binding '%v' now bound to '%v'", bindingName, newKey))
|
||||||
|
})
|
||||||
|
//}
|
||||||
|
|
||||||
|
//return events.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (kb *KeyBindingController) rebind(bindingName string, newKey string) error {
|
||||||
|
err := kb.service.Rebind(bindingName, newKey)
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var invalidBinding keybindings.InvalidBindingError
|
||||||
|
if !errors.As(err, &invalidBinding) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return kb.customBindingSource.Rebind(bindingName, newKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (kb *KeyBindingController) unbindKey(key string) {
|
||||||
|
kb.service.UnbindKey(key)
|
||||||
|
kb.customBindingSource.UnbindKey(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (kb *KeyBindingController) findExistingBinding(key string) string {
|
||||||
|
if binding := kb.service.LookupBinding(key); binding != "" {
|
||||||
|
return binding
|
||||||
|
}
|
||||||
|
|
||||||
|
return kb.customBindingSource.LookupBinding(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (kb *KeyBindingController) LookupCustomBinding(key string) tea.Cmd {
|
||||||
|
if kb.customBindingSource == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return kb.customBindingSource.CustomKeyCommand(key)
|
||||||
}
|
}
|
||||||
|
|
|
@ -214,3 +214,33 @@ func (s *sessionImpl) Query(ctx context.Context, query string, opts scriptmanage
|
||||||
}
|
}
|
||||||
return newResultSet, nil
|
return newResultSet, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (sc *ScriptController) CustomKeyCommand(key string) tea.Cmd {
|
||||||
|
_, cmd := sc.scriptManager.LookupKeyBinding(key)
|
||||||
|
if cmd == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return func() tea.Msg {
|
||||||
|
errChan := sc.waitAndPrintScriptError()
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
if err := cmd.Invoke(ctx, nil, errChan); err != nil {
|
||||||
|
return events.Error(err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sc *ScriptController) Rebind(bindingName string, newKey string) error {
|
||||||
|
return sc.scriptManager.RebindKeyBinding(bindingName, newKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sc *ScriptController) LookupBinding(theKey string) string {
|
||||||
|
bindingName, _ := sc.scriptManager.LookupKeyBinding(theKey)
|
||||||
|
return bindingName
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sc *ScriptController) UnbindKey(key string) {
|
||||||
|
sc.scriptManager.UnbindKey(key)
|
||||||
|
}
|
||||||
|
|
|
@ -1,9 +0,0 @@
|
||||||
package jobs
|
|
||||||
|
|
||||||
const (
|
|
||||||
JobEventForegroundDone = "job_foreground_done"
|
|
||||||
)
|
|
||||||
|
|
||||||
type JobDoneEvent struct {
|
|
||||||
Err error
|
|
||||||
}
|
|
|
@ -3,65 +3,60 @@ package jobs
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
bus "github.com/lmika/events"
|
bus "github.com/lmika/events"
|
||||||
|
"github.com/pkg/errors"
|
||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Job func(ctx context.Context)
|
|
||||||
|
|
||||||
type jobInfo struct {
|
type jobInfo struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
|
job Job
|
||||||
cancelFn func()
|
cancelFn func()
|
||||||
}
|
}
|
||||||
|
|
||||||
type Services struct {
|
type Services struct {
|
||||||
bus *bus.Bus
|
bus *bus.Bus
|
||||||
|
jobQueue chan Job
|
||||||
|
|
||||||
mutex *sync.Mutex
|
mutex *sync.Mutex
|
||||||
foregroundJob *jobInfo
|
foregroundJob *jobInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewService(bus *bus.Bus) *Services {
|
func NewService(bus *bus.Bus) *Services {
|
||||||
return &Services{
|
jc := &Services{
|
||||||
bus: bus,
|
bus: bus,
|
||||||
mutex: new(sync.Mutex),
|
jobQueue: make(chan Job, 10),
|
||||||
|
mutex: new(sync.Mutex),
|
||||||
}
|
}
|
||||||
|
go jc.waitForJobs()
|
||||||
|
return jc
|
||||||
}
|
}
|
||||||
|
|
||||||
// SubmitForegroundJob starts a foreground job.
|
// SubmitForegroundJob starts a foreground job.
|
||||||
func (jc *Services) SubmitForegroundJob(job Job, onJobUpdate func(msg string)) {
|
func (jc *Services) SubmitForegroundJob(job Job) error {
|
||||||
// TODO: if there's already a foreground job, then return error
|
select {
|
||||||
|
case jc.jobQueue <- job:
|
||||||
ctx, cancelFn := context.WithCancel(context.Background())
|
return nil
|
||||||
|
default:
|
||||||
jobUpdateChan := make(chan string)
|
return errors.New("too many jobs queued")
|
||||||
jobUpdater := &jobUpdaterValue{msgUpdate: jobUpdateChan}
|
|
||||||
ctx = context.WithValue(ctx, jobUpdaterKey, jobUpdater)
|
|
||||||
|
|
||||||
newJobInfo := &jobInfo{
|
|
||||||
ctx: ctx,
|
|
||||||
cancelFn: cancelFn,
|
|
||||||
}
|
}
|
||||||
// TODO: needs to be protected by the mutex
|
}
|
||||||
|
|
||||||
|
func (jc *Services) setForegroundJob(newJobInfo *jobInfo) {
|
||||||
|
jc.mutex.Lock()
|
||||||
jc.foregroundJob = newJobInfo
|
jc.foregroundJob = newJobInfo
|
||||||
|
jc.mutex.Unlock()
|
||||||
|
|
||||||
go func() {
|
if newJobInfo != nil {
|
||||||
defer cancelFn()
|
jc.bus.Fire(JobStartEvent, EventData{Job: newJobInfo.job})
|
||||||
defer close(jobUpdateChan)
|
} else {
|
||||||
|
jc.bus.Fire(JobIdleEvent)
|
||||||
job(newJobInfo.ctx)
|
}
|
||||||
|
|
||||||
// TODO: needs to be protected by the mutex
|
|
||||||
jc.foregroundJob = nil
|
|
||||||
}()
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
for update := range jobUpdateChan {
|
|
||||||
onJobUpdate(update)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (jc *Services) CancelForegroundJob() bool {
|
func (jc *Services) CancelForegroundJob() bool {
|
||||||
|
jc.mutex.Lock()
|
||||||
|
defer jc.mutex.Unlock()
|
||||||
|
|
||||||
// TODO: needs to be protected by the mutex
|
// TODO: needs to be protected by the mutex
|
||||||
if jc.foregroundJob != nil {
|
if jc.foregroundJob != nil {
|
||||||
// A nil cancel for a non-nil foreground job indicates that the cancellation function
|
// A nil cancel for a non-nil foreground job indicates that the cancellation function
|
||||||
|
@ -77,3 +72,46 @@ func (jc *Services) CancelForegroundJob() bool {
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (jc *Services) waitForJobs() {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
for job := range jc.jobQueue {
|
||||||
|
jc.runJob(ctx, job)
|
||||||
|
|
||||||
|
if len(jc.jobQueue) == 0 {
|
||||||
|
jc.setForegroundJob(nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (jc *Services) runJob(ctx context.Context, job Job) {
|
||||||
|
ctx, cancelFn := context.WithCancel(context.Background())
|
||||||
|
defer cancelFn()
|
||||||
|
|
||||||
|
updateCloseChan := make(chan struct{})
|
||||||
|
jobUpdateChan := make(chan string)
|
||||||
|
|
||||||
|
jobUpdater := &jobUpdaterValue{msgUpdate: jobUpdateChan}
|
||||||
|
ctx = context.WithValue(ctx, jobUpdaterKey, jobUpdater)
|
||||||
|
|
||||||
|
newJobInfo := &jobInfo{
|
||||||
|
job: job,
|
||||||
|
ctx: ctx,
|
||||||
|
cancelFn: cancelFn,
|
||||||
|
}
|
||||||
|
jc.setForegroundJob(newJobInfo)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer close(updateCloseChan)
|
||||||
|
|
||||||
|
for update := range jobUpdateChan {
|
||||||
|
jc.bus.Fire(JobUpdateEvent, EventData{Job: job}, update)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
job.Execute(newJobInfo.ctx)
|
||||||
|
|
||||||
|
close(jobUpdateChan)
|
||||||
|
<-updateCloseChan
|
||||||
|
}
|
||||||
|
|
32
internal/dynamo-browse/services/jobs/models.go
Normal file
32
internal/dynamo-browse/services/jobs/models.go
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
package jobs
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
const (
|
||||||
|
JobStartEvent = "jobs.start"
|
||||||
|
JobIdleEvent = "jobs.idle"
|
||||||
|
JobUpdateEvent = "jobs.update"
|
||||||
|
)
|
||||||
|
|
||||||
|
type EventData struct {
|
||||||
|
Job Job
|
||||||
|
}
|
||||||
|
|
||||||
|
type Job interface {
|
||||||
|
Execute(ctx context.Context)
|
||||||
|
}
|
||||||
|
|
||||||
|
type JobFunc func(ctx context.Context)
|
||||||
|
|
||||||
|
func (jf JobFunc) Execute(ctx context.Context) {
|
||||||
|
jf(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithDescription(description string, job Job) Job {
|
||||||
|
return DescribableJob{job, description}
|
||||||
|
}
|
||||||
|
|
||||||
|
type DescribableJob struct {
|
||||||
|
Job
|
||||||
|
Description string
|
||||||
|
}
|
|
@ -12,3 +12,9 @@ type KeyAlreadyBoundError struct {
|
||||||
func (e KeyAlreadyBoundError) Error() string {
|
func (e KeyAlreadyBoundError) Error() string {
|
||||||
return fmt.Sprintf("key '%v' already bound to '%v'", e.Key, e.ExistingBindingName)
|
return fmt.Sprintf("key '%v' already bound to '%v'", e.Key, e.ExistingBindingName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type InvalidBindingError string
|
||||||
|
|
||||||
|
func (e InvalidBindingError) Error() string {
|
||||||
|
return fmt.Sprintf("invalid binding: %v", string(e))
|
||||||
|
}
|
||||||
|
|
|
@ -2,8 +2,6 @@ package keybindings
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/charmbracelet/bubbles/key"
|
"github.com/charmbracelet/bubbles/key"
|
||||||
"github.com/pkg/errors"
|
|
||||||
"log"
|
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
@ -23,37 +21,56 @@ func NewService(keyBinding any) *Service {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) Rebind(name string, newKey string, force bool) error {
|
func (s *Service) LookupBinding(theKey string) string {
|
||||||
// Check if there already exists a binding (or clear it)
|
|
||||||
var foundBinding = ""
|
var foundBinding = ""
|
||||||
s.walkBindingFields(func(bindingName string, binding *key.Binding) bool {
|
s.walkBindingFields(func(bindingName string, binding *key.Binding) bool {
|
||||||
for _, boundKey := range binding.Keys() {
|
for _, boundKey := range binding.Keys() {
|
||||||
if boundKey == newKey {
|
if boundKey == theKey {
|
||||||
if force {
|
foundBinding = bindingName
|
||||||
// TODO: only filter out "boundKey" rather clear
|
return false
|
||||||
log.Printf("clearing binding of %v", bindingName)
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
return foundBinding
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) UnbindKey(theKey string) {
|
||||||
|
s.walkBindingFields(func(bindingName string, binding *key.Binding) bool {
|
||||||
|
for _, boundKey := range binding.Keys() {
|
||||||
|
if boundKey == theKey {
|
||||||
|
l := len(binding.Keys())
|
||||||
|
if l == 1 {
|
||||||
*binding = key.NewBinding()
|
*binding = key.NewBinding()
|
||||||
return true
|
} else if l > 1 {
|
||||||
} else {
|
newKeys := make([]string, 0)
|
||||||
foundBinding = bindingName
|
for _, k := range binding.Keys() {
|
||||||
return false
|
if k != theKey {
|
||||||
|
newKeys = append(newKeys, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*binding = key.NewBinding(key.WithKeys(newKeys...))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if foundBinding != "" {
|
func (s *Service) Rebind(name string, newKey string) error {
|
||||||
return KeyAlreadyBoundError{Key: newKey, ExistingBindingName: foundBinding}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rebind
|
// Rebind
|
||||||
binding := s.findFieldForBinding(name)
|
binding := s.findFieldForBinding(name)
|
||||||
if binding == nil {
|
if binding == nil {
|
||||||
return errors.Errorf("invalid binding: %v", name)
|
return InvalidBindingError(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(binding.Keys()) == 0 {
|
||||||
|
*binding = key.NewBinding(key.WithKeys(newKey))
|
||||||
|
} else {
|
||||||
|
newKeys := append([]string{newKey}, binding.Keys()...)
|
||||||
|
*binding = key.NewBinding(key.WithKeys(newKeys...))
|
||||||
}
|
}
|
||||||
|
|
||||||
*binding = key.NewBinding(key.WithKeys(newKey))
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Code generated by mockery v2.16.0. DO NOT EDIT.
|
// Code generated by mockery v2.20.0. DO NOT EDIT.
|
||||||
|
|
||||||
package mocks
|
package mocks
|
||||||
|
|
||||||
|
@ -29,6 +29,10 @@ func (_m *SessionService) Query(ctx context.Context, expr string, queryOptions s
|
||||||
ret := _m.Called(ctx, expr, queryOptions)
|
ret := _m.Called(ctx, expr, queryOptions)
|
||||||
|
|
||||||
var r0 *models.ResultSet
|
var r0 *models.ResultSet
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, string, scriptmanager.QueryOptions) (*models.ResultSet, error)); ok {
|
||||||
|
return rf(ctx, expr, queryOptions)
|
||||||
|
}
|
||||||
if rf, ok := ret.Get(0).(func(context.Context, string, scriptmanager.QueryOptions) *models.ResultSet); ok {
|
if rf, ok := ret.Get(0).(func(context.Context, string, scriptmanager.QueryOptions) *models.ResultSet); ok {
|
||||||
r0 = rf(ctx, expr, queryOptions)
|
r0 = rf(ctx, expr, queryOptions)
|
||||||
} else {
|
} else {
|
||||||
|
@ -37,7 +41,6 @@ func (_m *SessionService) Query(ctx context.Context, expr string, queryOptions s
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var r1 error
|
|
||||||
if rf, ok := ret.Get(1).(func(context.Context, string, scriptmanager.QueryOptions) error); ok {
|
if rf, ok := ret.Get(1).(func(context.Context, string, scriptmanager.QueryOptions) error); ok {
|
||||||
r1 = rf(ctx, expr, queryOptions)
|
r1 = rf(ctx, expr, queryOptions)
|
||||||
} else {
|
} else {
|
||||||
|
@ -53,9 +56,9 @@ type SessionService_Query_Call struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Query is a helper method to define mock.On call
|
// Query is a helper method to define mock.On call
|
||||||
// - ctx context.Context
|
// - ctx context.Context
|
||||||
// - expr string
|
// - expr string
|
||||||
// - queryOptions scriptmanager.QueryOptions
|
// - queryOptions scriptmanager.QueryOptions
|
||||||
func (_e *SessionService_Expecter) Query(ctx interface{}, expr interface{}, queryOptions interface{}) *SessionService_Query_Call {
|
func (_e *SessionService_Expecter) Query(ctx interface{}, expr interface{}, queryOptions interface{}) *SessionService_Query_Call {
|
||||||
return &SessionService_Query_Call{Call: _e.mock.On("Query", ctx, expr, queryOptions)}
|
return &SessionService_Query_Call{Call: _e.mock.On("Query", ctx, expr, queryOptions)}
|
||||||
}
|
}
|
||||||
|
@ -72,6 +75,11 @@ func (_c *SessionService_Query_Call) Return(_a0 *models.ResultSet, _a1 error) *S
|
||||||
return _c
|
return _c
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (_c *SessionService_Query_Call) RunAndReturn(run func(context.Context, string, scriptmanager.QueryOptions) (*models.ResultSet, error)) *SessionService_Query_Call {
|
||||||
|
_c.Call.Return(run)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
// ResultSet provides a mock function with given fields: ctx
|
// ResultSet provides a mock function with given fields: ctx
|
||||||
func (_m *SessionService) ResultSet(ctx context.Context) *models.ResultSet {
|
func (_m *SessionService) ResultSet(ctx context.Context) *models.ResultSet {
|
||||||
ret := _m.Called(ctx)
|
ret := _m.Called(ctx)
|
||||||
|
@ -94,7 +102,7 @@ type SessionService_ResultSet_Call struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResultSet is a helper method to define mock.On call
|
// ResultSet is a helper method to define mock.On call
|
||||||
// - ctx context.Context
|
// - ctx context.Context
|
||||||
func (_e *SessionService_Expecter) ResultSet(ctx interface{}) *SessionService_ResultSet_Call {
|
func (_e *SessionService_Expecter) ResultSet(ctx interface{}) *SessionService_ResultSet_Call {
|
||||||
return &SessionService_ResultSet_Call{Call: _e.mock.On("ResultSet", ctx)}
|
return &SessionService_ResultSet_Call{Call: _e.mock.On("ResultSet", ctx)}
|
||||||
}
|
}
|
||||||
|
@ -111,6 +119,11 @@ func (_c *SessionService_ResultSet_Call) Return(_a0 *models.ResultSet) *SessionS
|
||||||
return _c
|
return _c
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (_c *SessionService_ResultSet_Call) RunAndReturn(run func(context.Context) *models.ResultSet) *SessionService_ResultSet_Call {
|
||||||
|
_c.Call.Return(run)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
// SelectedItemIndex provides a mock function with given fields: ctx
|
// SelectedItemIndex provides a mock function with given fields: ctx
|
||||||
func (_m *SessionService) SelectedItemIndex(ctx context.Context) int {
|
func (_m *SessionService) SelectedItemIndex(ctx context.Context) int {
|
||||||
ret := _m.Called(ctx)
|
ret := _m.Called(ctx)
|
||||||
|
@ -131,7 +144,7 @@ type SessionService_SelectedItemIndex_Call struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// SelectedItemIndex is a helper method to define mock.On call
|
// SelectedItemIndex is a helper method to define mock.On call
|
||||||
// - ctx context.Context
|
// - ctx context.Context
|
||||||
func (_e *SessionService_Expecter) SelectedItemIndex(ctx interface{}) *SessionService_SelectedItemIndex_Call {
|
func (_e *SessionService_Expecter) SelectedItemIndex(ctx interface{}) *SessionService_SelectedItemIndex_Call {
|
||||||
return &SessionService_SelectedItemIndex_Call{Call: _e.mock.On("SelectedItemIndex", ctx)}
|
return &SessionService_SelectedItemIndex_Call{Call: _e.mock.On("SelectedItemIndex", ctx)}
|
||||||
}
|
}
|
||||||
|
@ -148,6 +161,11 @@ func (_c *SessionService_SelectedItemIndex_Call) Return(_a0 int) *SessionService
|
||||||
return _c
|
return _c
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (_c *SessionService_SelectedItemIndex_Call) RunAndReturn(run func(context.Context) int) *SessionService_SelectedItemIndex_Call {
|
||||||
|
_c.Call.Return(run)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
// SetResultSet provides a mock function with given fields: ctx, newResultSet
|
// SetResultSet provides a mock function with given fields: ctx, newResultSet
|
||||||
func (_m *SessionService) SetResultSet(ctx context.Context, newResultSet *models.ResultSet) {
|
func (_m *SessionService) SetResultSet(ctx context.Context, newResultSet *models.ResultSet) {
|
||||||
_m.Called(ctx, newResultSet)
|
_m.Called(ctx, newResultSet)
|
||||||
|
@ -159,8 +177,8 @@ type SessionService_SetResultSet_Call struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetResultSet is a helper method to define mock.On call
|
// SetResultSet is a helper method to define mock.On call
|
||||||
// - ctx context.Context
|
// - ctx context.Context
|
||||||
// - newResultSet *models.ResultSet
|
// - newResultSet *models.ResultSet
|
||||||
func (_e *SessionService_Expecter) SetResultSet(ctx interface{}, newResultSet interface{}) *SessionService_SetResultSet_Call {
|
func (_e *SessionService_Expecter) SetResultSet(ctx interface{}, newResultSet interface{}) *SessionService_SetResultSet_Call {
|
||||||
return &SessionService_SetResultSet_Call{Call: _e.mock.On("SetResultSet", ctx, newResultSet)}
|
return &SessionService_SetResultSet_Call{Call: _e.mock.On("SetResultSet", ctx, newResultSet)}
|
||||||
}
|
}
|
||||||
|
@ -177,6 +195,11 @@ func (_c *SessionService_SetResultSet_Call) Return() *SessionService_SetResultSe
|
||||||
return _c
|
return _c
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (_c *SessionService_SetResultSet_Call) RunAndReturn(run func(context.Context, *models.ResultSet)) *SessionService_SetResultSet_Call {
|
||||||
|
_c.Call.Return(run)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
type mockConstructorTestingTNewSessionService interface {
|
type mockConstructorTestingTNewSessionService interface {
|
||||||
mock.TestingT
|
mock.TestingT
|
||||||
Cleanup(func())
|
Cleanup(func())
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Code generated by mockery v2.16.0. DO NOT EDIT.
|
// Code generated by mockery v2.20.0. DO NOT EDIT.
|
||||||
|
|
||||||
package mocks
|
package mocks
|
||||||
|
|
||||||
|
@ -32,8 +32,8 @@ type UIService_PrintMessage_Call struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// PrintMessage is a helper method to define mock.On call
|
// PrintMessage is a helper method to define mock.On call
|
||||||
// - ctx context.Context
|
// - ctx context.Context
|
||||||
// - msg string
|
// - msg string
|
||||||
func (_e *UIService_Expecter) PrintMessage(ctx interface{}, msg interface{}) *UIService_PrintMessage_Call {
|
func (_e *UIService_Expecter) PrintMessage(ctx interface{}, msg interface{}) *UIService_PrintMessage_Call {
|
||||||
return &UIService_PrintMessage_Call{Call: _e.mock.On("PrintMessage", ctx, msg)}
|
return &UIService_PrintMessage_Call{Call: _e.mock.On("PrintMessage", ctx, msg)}
|
||||||
}
|
}
|
||||||
|
@ -50,6 +50,11 @@ func (_c *UIService_PrintMessage_Call) Return() *UIService_PrintMessage_Call {
|
||||||
return _c
|
return _c
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (_c *UIService_PrintMessage_Call) RunAndReturn(run func(context.Context, string)) *UIService_PrintMessage_Call {
|
||||||
|
_c.Call.Return(run)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
// Prompt provides a mock function with given fields: ctx, msg
|
// Prompt provides a mock function with given fields: ctx, msg
|
||||||
func (_m *UIService) Prompt(ctx context.Context, msg string) chan string {
|
func (_m *UIService) Prompt(ctx context.Context, msg string) chan string {
|
||||||
ret := _m.Called(ctx, msg)
|
ret := _m.Called(ctx, msg)
|
||||||
|
@ -72,8 +77,8 @@ type UIService_Prompt_Call struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prompt is a helper method to define mock.On call
|
// Prompt is a helper method to define mock.On call
|
||||||
// - ctx context.Context
|
// - ctx context.Context
|
||||||
// - msg string
|
// - msg string
|
||||||
func (_e *UIService_Expecter) Prompt(ctx interface{}, msg interface{}) *UIService_Prompt_Call {
|
func (_e *UIService_Expecter) Prompt(ctx interface{}, msg interface{}) *UIService_Prompt_Call {
|
||||||
return &UIService_Prompt_Call{Call: _e.mock.On("Prompt", ctx, msg)}
|
return &UIService_Prompt_Call{Call: _e.mock.On("Prompt", ctx, msg)}
|
||||||
}
|
}
|
||||||
|
@ -90,6 +95,11 @@ func (_c *UIService_Prompt_Call) Return(_a0 chan string) *UIService_Prompt_Call
|
||||||
return _c
|
return _c
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (_c *UIService_Prompt_Call) RunAndReturn(run func(context.Context, string) chan string) *UIService_Prompt_Call {
|
||||||
|
_c.Call.Return(run)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
type mockConstructorTestingTNewUIService interface {
|
type mockConstructorTestingTNewUIService interface {
|
||||||
mock.TestingT
|
mock.TestingT
|
||||||
Cleanup(func())
|
Cleanup(func())
|
||||||
|
|
|
@ -2,10 +2,16 @@ package scriptmanager
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"github.com/cloudcmds/tamarin/arg"
|
"github.com/cloudcmds/tamarin/arg"
|
||||||
"github.com/cloudcmds/tamarin/object"
|
"github.com/cloudcmds/tamarin/object"
|
||||||
"github.com/cloudcmds/tamarin/scope"
|
"github.com/cloudcmds/tamarin/scope"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
"regexp"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
validKeyBindingNames = regexp.MustCompile(`^[-a-zA-Z0-9_]+$`)
|
||||||
)
|
)
|
||||||
|
|
||||||
type extModule struct {
|
type extModule struct {
|
||||||
|
@ -18,6 +24,7 @@ func (m *extModule) register(scp *scope.Scope) {
|
||||||
|
|
||||||
modScope.AddBuiltins([]*object.Builtin{
|
modScope.AddBuiltins([]*object.Builtin{
|
||||||
object.NewBuiltin("command", m.command, mod),
|
object.NewBuiltin("command", m.command, mod),
|
||||||
|
object.NewBuiltin("key_binding", m.keyBinding, mod),
|
||||||
})
|
})
|
||||||
|
|
||||||
scp.Declare("ext", mod, true)
|
scp.Declare("ext", mod, true)
|
||||||
|
@ -65,3 +72,64 @@ func (m *extModule) command(ctx context.Context, args ...object.Object) object.O
|
||||||
m.scriptPlugin.definedCommands[cmdName] = &Command{plugin: m.scriptPlugin, cmdFn: newCommand}
|
m.scriptPlugin.definedCommands[cmdName] = &Command{plugin: m.scriptPlugin, cmdFn: newCommand}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *extModule) keyBinding(ctx context.Context, args ...object.Object) object.Object {
|
||||||
|
if err := arg.Require("ext.key_binding", 3, args); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
bindingName, err := object.AsString(args[0])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if !validKeyBindingNames.MatchString(bindingName) {
|
||||||
|
return object.NewError(errors.New("value error: binding name must match regexp [-a-zA-Z0-9_]+"))
|
||||||
|
}
|
||||||
|
|
||||||
|
options, err := object.AsMap(args[1])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultKey string
|
||||||
|
if strVal, isStrVal := options.Get("default").(*object.String); isStrVal {
|
||||||
|
defaultKey = strVal.Value()
|
||||||
|
}
|
||||||
|
|
||||||
|
fnRes, isFnRes := args[2].(*object.Function)
|
||||||
|
if !isFnRes {
|
||||||
|
return object.NewError(errors.New("expected second arg to be a function"))
|
||||||
|
}
|
||||||
|
|
||||||
|
callFn, hasCallFn := object.GetCallFunc(ctx)
|
||||||
|
if !hasCallFn {
|
||||||
|
return object.NewError(errors.New("no callFn found in context"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// This command function will be executed by the script scheduler
|
||||||
|
newCommand := func(ctx context.Context, args []string) error {
|
||||||
|
objArgs := make([]object.Object, len(args))
|
||||||
|
for i, a := range args {
|
||||||
|
objArgs[i] = object.NewString(a)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx = ctxWithOptions(ctx, m.scriptPlugin.scriptService.options)
|
||||||
|
|
||||||
|
res := callFn(ctx, fnRes.Scope(), fnRes, objArgs)
|
||||||
|
if object.IsError(res) {
|
||||||
|
errObj := res.(*object.Error)
|
||||||
|
return errors.Errorf("command error '%v':%v - %v", m.scriptPlugin.name, bindingName, errObj.Inspect())
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
fullBindingName := fmt.Sprintf("ext.%v.%v", m.scriptPlugin.name, bindingName)
|
||||||
|
|
||||||
|
if m.scriptPlugin.definedKeyBindings == nil {
|
||||||
|
m.scriptPlugin.definedKeyBindings = make(map[string]*Command)
|
||||||
|
m.scriptPlugin.keyToKeyBinding = make(map[string]string)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.scriptPlugin.definedKeyBindings[fullBindingName] = &Command{plugin: m.scriptPlugin, cmdFn: newCommand}
|
||||||
|
m.scriptPlugin.keyToKeyBinding[defaultKey] = fullBindingName
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
@ -33,8 +33,15 @@ func (um *sessionModule) query(ctx context.Context, args ...object.Object) objec
|
||||||
}
|
}
|
||||||
|
|
||||||
// Table name
|
// Table name
|
||||||
if val, isVal := objMap.Get("table").(*object.String); isVal && val.Value() != "" {
|
if val := objMap.Get("table"); val != object.Nil && val.IsTruthy() {
|
||||||
options.TableName = val.Value()
|
switch tv := val.(type) {
|
||||||
|
case *object.String:
|
||||||
|
options.TableName = tv.Value()
|
||||||
|
case *tableProxy:
|
||||||
|
options.TableName = tv.table.Name
|
||||||
|
default:
|
||||||
|
return object.Errorf("type error: query option 'table' must be either a string or table")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Placeholders
|
// Placeholders
|
||||||
|
@ -111,12 +118,26 @@ func (um *sessionModule) setResultSet(ctx context.Context, args ...object.Object
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (um *sessionModule) currentTable(ctx context.Context, args ...object.Object) object.Object {
|
||||||
|
if err := arg.Require("session.current_table", 0, args); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
rs := um.sessionService.ResultSet(ctx)
|
||||||
|
if rs == nil {
|
||||||
|
return object.Nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &tableProxy{table: rs.TableInfo}
|
||||||
|
}
|
||||||
|
|
||||||
func (um *sessionModule) register(scp *scope.Scope) {
|
func (um *sessionModule) register(scp *scope.Scope) {
|
||||||
modScope := scope.New(scope.Opts{})
|
modScope := scope.New(scope.Opts{})
|
||||||
mod := object.NewModule("session", modScope)
|
mod := object.NewModule("session", modScope)
|
||||||
|
|
||||||
modScope.AddBuiltins([]*object.Builtin{
|
modScope.AddBuiltins([]*object.Builtin{
|
||||||
object.NewBuiltin("query", um.query, mod),
|
object.NewBuiltin("query", um.query, mod),
|
||||||
|
object.NewBuiltin("current_table", um.currentTable, mod),
|
||||||
object.NewBuiltin("result_set", um.resultSet, mod),
|
object.NewBuiltin("result_set", um.resultSet, mod),
|
||||||
object.NewBuiltin("selected_item", um.selectedItem, mod),
|
object.NewBuiltin("selected_item", um.selectedItem, mod),
|
||||||
object.NewBuiltin("set_result_set", um.setResultSet, mod),
|
object.NewBuiltin("set_result_set", um.setResultSet, mod),
|
||||||
|
|
|
@ -12,6 +12,78 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestModSession_Table(t *testing.T) {
|
||||||
|
t.Run("should return details of the current table", func(t *testing.T) {
|
||||||
|
tableDef := models.TableInfo{
|
||||||
|
Name: "test_table",
|
||||||
|
Keys: models.KeyAttribute{
|
||||||
|
PartitionKey: "pk",
|
||||||
|
SortKey: "sk",
|
||||||
|
},
|
||||||
|
GSIs: []models.TableGSI{
|
||||||
|
{
|
||||||
|
Name: "index-1",
|
||||||
|
Keys: models.KeyAttribute{
|
||||||
|
PartitionKey: "ipk",
|
||||||
|
SortKey: "isk",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
rs := models.ResultSet{TableInfo: &tableDef}
|
||||||
|
|
||||||
|
mockedSessionService := mocks.NewSessionService(t)
|
||||||
|
mockedSessionService.EXPECT().ResultSet(mock.Anything).Return(&rs)
|
||||||
|
|
||||||
|
testFS := testScriptFile(t, "test.tm", `
|
||||||
|
table := session.current_table()
|
||||||
|
|
||||||
|
assert(table.name == "test_table")
|
||||||
|
assert(table.keys["partition"] == "pk")
|
||||||
|
assert(table.keys["sort"] == "sk")
|
||||||
|
assert(len(table.gsis) == 1)
|
||||||
|
assert(table.gsis[0].name == "index-1")
|
||||||
|
assert(table.gsis[0].keys["partition"] == "ipk")
|
||||||
|
assert(table.gsis[0].keys["sort"] == "isk")
|
||||||
|
|
||||||
|
assert(table == session.result_set().table)
|
||||||
|
`)
|
||||||
|
|
||||||
|
srv := scriptmanager.New(scriptmanager.WithFS(testFS))
|
||||||
|
srv.SetIFaces(scriptmanager.Ifaces{
|
||||||
|
Session: mockedSessionService,
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
err := <-srv.RunAdHocScript(ctx, "test.tm")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
mockedSessionService.AssertExpectations(t)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should return nil if no current result set", func(t *testing.T) {
|
||||||
|
mockedSessionService := mocks.NewSessionService(t)
|
||||||
|
mockedSessionService.EXPECT().ResultSet(mock.Anything).Return(nil)
|
||||||
|
|
||||||
|
testFS := testScriptFile(t, "test.tm", `
|
||||||
|
table := session.current_table()
|
||||||
|
|
||||||
|
assert(table == nil)
|
||||||
|
`)
|
||||||
|
|
||||||
|
srv := scriptmanager.New(scriptmanager.WithFS(testFS))
|
||||||
|
srv.SetIFaces(scriptmanager.Ifaces{
|
||||||
|
Session: mockedSessionService,
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
err := <-srv.RunAdHocScript(ctx, "test.tm")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
mockedSessionService.AssertExpectations(t)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestModSession_Query(t *testing.T) {
|
func TestModSession_Query(t *testing.T) {
|
||||||
t.Run("should successfully return query result", func(t *testing.T) {
|
t.Run("should successfully return query result", func(t *testing.T) {
|
||||||
rs := &models.ResultSet{}
|
rs := &models.ResultSet{}
|
||||||
|
@ -110,6 +182,42 @@ func TestModSession_Query(t *testing.T) {
|
||||||
mockedSessionService.AssertExpectations(t)
|
mockedSessionService.AssertExpectations(t)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("should successfully specify table proxy", func(t *testing.T) {
|
||||||
|
rs := &models.ResultSet{}
|
||||||
|
|
||||||
|
mockedSessionService := mocks.NewSessionService(t)
|
||||||
|
mockedSessionService.EXPECT().ResultSet(mock.Anything).Return(&models.ResultSet{
|
||||||
|
TableInfo: &models.TableInfo{
|
||||||
|
Name: "some-resultset-table",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
mockedSessionService.EXPECT().Query(mock.Anything, "some expr", scriptmanager.QueryOptions{
|
||||||
|
TableName: "some-resultset-table",
|
||||||
|
}).Return(rs, nil)
|
||||||
|
|
||||||
|
mockedUIService := mocks.NewUIService(t)
|
||||||
|
|
||||||
|
testFS := testScriptFile(t, "test.tm", `
|
||||||
|
res := session.query("some expr", {
|
||||||
|
table: session.result_set().table,
|
||||||
|
})
|
||||||
|
assert(!res.is_err())
|
||||||
|
`)
|
||||||
|
|
||||||
|
srv := scriptmanager.New(scriptmanager.WithFS(testFS))
|
||||||
|
srv.SetIFaces(scriptmanager.Ifaces{
|
||||||
|
UI: mockedUIService,
|
||||||
|
Session: mockedSessionService,
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
err := <-srv.RunAdHocScript(ctx, "test.tm")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
mockedUIService.AssertExpectations(t)
|
||||||
|
mockedSessionService.AssertExpectations(t)
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("should set placeholder values", func(t *testing.T) {
|
t.Run("should set placeholder values", func(t *testing.T) {
|
||||||
rs := &models.ResultSet{}
|
rs := &models.ResultSet{}
|
||||||
|
|
||||||
|
|
|
@ -91,6 +91,8 @@ func (r *resultSetProxy) Iter() object.Iterator {
|
||||||
|
|
||||||
func (r *resultSetProxy) GetAttr(name string) (object.Object, bool) {
|
func (r *resultSetProxy) GetAttr(name string) (object.Object, bool) {
|
||||||
switch name {
|
switch name {
|
||||||
|
case "table":
|
||||||
|
return &tableProxy{table: r.resultSet.TableInfo}, true
|
||||||
case "length":
|
case "length":
|
||||||
return object.NewInt(int64(len(r.resultSet.Items()))), true
|
return object.NewInt(int64(len(r.resultSet.Items()))), true
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,11 @@ import (
|
||||||
|
|
||||||
func TestResultSetProxy(t *testing.T) {
|
func TestResultSetProxy(t *testing.T) {
|
||||||
t.Run("should property return properties of a resultset and item", func(t *testing.T) {
|
t.Run("should property return properties of a resultset and item", func(t *testing.T) {
|
||||||
rs := &models.ResultSet{}
|
rs := &models.ResultSet{
|
||||||
|
TableInfo: &models.TableInfo{
|
||||||
|
Name: "test-table",
|
||||||
|
},
|
||||||
|
}
|
||||||
rs.SetItems([]models.Item{
|
rs.SetItems([]models.Item{
|
||||||
{"pk": &types.AttributeValueMemberS{Value: "abc"}},
|
{"pk": &types.AttributeValueMemberS{Value: "abc"}},
|
||||||
{"pk": &types.AttributeValueMemberS{Value: "1232"}},
|
{"pk": &types.AttributeValueMemberS{Value: "1232"}},
|
||||||
|
@ -28,6 +32,8 @@ func TestResultSetProxy(t *testing.T) {
|
||||||
res := session.query("some expr").unwrap()
|
res := session.query("some expr").unwrap()
|
||||||
|
|
||||||
// Test properties of the result set
|
// Test properties of the result set
|
||||||
|
assert(res.table.name, "hello")
|
||||||
|
|
||||||
assert(res == res, "result_set.equals")
|
assert(res == res, "result_set.equals")
|
||||||
assert(res.length == 2, "result_set.length")
|
assert(res.length == 2, "result_set.length")
|
||||||
|
|
||||||
|
|
|
@ -4,10 +4,12 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"github.com/cloudcmds/tamarin/exec"
|
"github.com/cloudcmds/tamarin/exec"
|
||||||
"github.com/cloudcmds/tamarin/scope"
|
"github.com/cloudcmds/tamarin/scope"
|
||||||
|
"github.com/lmika/audax/internal/dynamo-browse/services/keybindings"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
|
@ -119,7 +121,7 @@ func (s *Service) loadScript(ctx context.Context, filename string, resChan chan
|
||||||
}
|
}
|
||||||
|
|
||||||
newPlugin := &ScriptPlugin{
|
newPlugin := &ScriptPlugin{
|
||||||
name: filepath.Base(filename),
|
name: strings.TrimSuffix(filepath.Base(filename), filepath.Ext(filename)),
|
||||||
scriptService: s,
|
scriptService: s,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -176,6 +178,49 @@ func (s *Service) LookupCommand(name string) *Command {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Service) LookupKeyBinding(key string) (string, *Command) {
|
||||||
|
for _, p := range s.plugins {
|
||||||
|
if bindingName, hasBinding := p.keyToKeyBinding[key]; hasBinding {
|
||||||
|
if cmd, hasCmd := p.definedKeyBindings[bindingName]; hasCmd {
|
||||||
|
return bindingName, cmd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) UnbindKey(key string) {
|
||||||
|
for _, p := range s.plugins {
|
||||||
|
if _, hasBinding := p.keyToKeyBinding[key]; hasBinding {
|
||||||
|
delete(p.keyToKeyBinding, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) RebindKeyBinding(keyBinding string, newKey string) error {
|
||||||
|
if newKey == "" {
|
||||||
|
for _, p := range s.plugins {
|
||||||
|
for k, b := range p.keyToKeyBinding {
|
||||||
|
if b == keyBinding {
|
||||||
|
delete(p.keyToKeyBinding, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, p := range s.plugins {
|
||||||
|
if _, hasCmd := p.definedKeyBindings[keyBinding]; hasCmd {
|
||||||
|
if newKey != "" {
|
||||||
|
p.keyToKeyBinding[newKey] = keyBinding
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return keybindings.InvalidBindingError(keyBinding)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Service) parentScope() *scope.Scope {
|
func (s *Service) parentScope() *scope.Scope {
|
||||||
scp := scope.New(scope.Opts{})
|
scp := scope.New(scope.Opts{})
|
||||||
(&uiModule{uiService: s.ifaces.UI}).register(scp)
|
(&uiModule{uiService: s.ifaces.UI}).register(scp)
|
||||||
|
|
|
@ -55,7 +55,7 @@ func TestService_LoadScript(t *testing.T) {
|
||||||
plugin, err := srv.LoadScript(ctx, "test.tm")
|
plugin, err := srv.LoadScript(ctx, "test.tm")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.NotNil(t, plugin)
|
assert.NotNil(t, plugin)
|
||||||
assert.Equal(t, "test.tm", plugin.Name())
|
assert.Equal(t, "test", plugin.Name())
|
||||||
|
|
||||||
cmd := srv.LookupCommand("somewhere")
|
cmd := srv.LookupCommand("somewhere")
|
||||||
assert.NotNil(t, cmd)
|
assert.NotNil(t, cmd)
|
||||||
|
|
105
internal/dynamo-browse/services/scriptmanager/tableproxy.go
Normal file
105
internal/dynamo-browse/services/scriptmanager/tableproxy.go
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
package scriptmanager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/cloudcmds/tamarin/object"
|
||||||
|
"github.com/lmika/audax/internal/common/sliceutils"
|
||||||
|
"github.com/lmika/audax/internal/dynamo-browse/models"
|
||||||
|
"reflect"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
tableProxyPartitionKey = "partition"
|
||||||
|
tableProxySortKey = "sort"
|
||||||
|
)
|
||||||
|
|
||||||
|
type tableProxy struct {
|
||||||
|
table *models.TableInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *tableProxy) Type() object.Type {
|
||||||
|
return "table"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *tableProxy) Inspect() string {
|
||||||
|
return "table(" + t.table.Name + ")"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *tableProxy) Interface() interface{} {
|
||||||
|
return t.table
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *tableProxy) Equals(other object.Object) object.Object {
|
||||||
|
otherT, isOtherRS := other.(*tableProxy)
|
||||||
|
if !isOtherRS {
|
||||||
|
return object.False
|
||||||
|
}
|
||||||
|
|
||||||
|
return object.NewBool(reflect.DeepEqual(t.table, otherT.table))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *tableProxy) GetAttr(name string) (object.Object, bool) {
|
||||||
|
switch name {
|
||||||
|
case "name":
|
||||||
|
return object.NewString(t.table.Name), true
|
||||||
|
case "keys":
|
||||||
|
return object.NewMap(map[string]object.Object{
|
||||||
|
tableProxyPartitionKey: object.NewString(t.table.Keys.PartitionKey),
|
||||||
|
tableProxySortKey: object.NewString(t.table.Keys.SortKey),
|
||||||
|
}), true
|
||||||
|
case "gsis":
|
||||||
|
return object.NewList(sliceutils.Map(t.table.GSIs, newTableIndexProxy)), true
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *tableProxy) IsTruthy() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
type tableIndexProxy struct {
|
||||||
|
gsi models.TableGSI
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTableIndexProxy(gsi models.TableGSI) object.Object {
|
||||||
|
return tableIndexProxy{gsi: gsi}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t tableIndexProxy) Type() object.Type {
|
||||||
|
return "index"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t tableIndexProxy) Inspect() string {
|
||||||
|
return "index(gsi," + t.gsi.Name + ")"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t tableIndexProxy) Interface() interface{} {
|
||||||
|
return t.gsi
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t tableIndexProxy) Equals(other object.Object) object.Object {
|
||||||
|
otherIP, isOtherIP := other.(tableIndexProxy)
|
||||||
|
if !isOtherIP {
|
||||||
|
return object.False
|
||||||
|
}
|
||||||
|
|
||||||
|
return object.NewBool(reflect.DeepEqual(t.gsi, otherIP.gsi))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t tableIndexProxy) GetAttr(name string) (object.Object, bool) {
|
||||||
|
switch name {
|
||||||
|
case "name":
|
||||||
|
return object.NewString(t.gsi.Name), true
|
||||||
|
case "keys":
|
||||||
|
return object.NewMap(map[string]object.Object{
|
||||||
|
tableProxyPartitionKey: object.NewString(t.gsi.Keys.PartitionKey),
|
||||||
|
tableProxySortKey: object.NewString(t.gsi.Keys.SortKey),
|
||||||
|
}), true
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t tableIndexProxy) IsTruthy() bool {
|
||||||
|
return true
|
||||||
|
}
|
|
@ -3,9 +3,11 @@ package scriptmanager
|
||||||
import "context"
|
import "context"
|
||||||
|
|
||||||
type ScriptPlugin struct {
|
type ScriptPlugin struct {
|
||||||
scriptService *Service
|
scriptService *Service
|
||||||
name string
|
name string
|
||||||
definedCommands map[string]*Command
|
definedCommands map[string]*Command
|
||||||
|
definedKeyBindings map[string]*Command
|
||||||
|
keyToKeyBinding map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sp *ScriptPlugin) Name() string {
|
func (sp *ScriptPlugin) Name() string {
|
||||||
|
|
|
@ -54,11 +54,12 @@ type Model struct {
|
||||||
|
|
||||||
mainViewIndex int
|
mainViewIndex int
|
||||||
|
|
||||||
root tea.Model
|
root tea.Model
|
||||||
tableView *dynamotableview.Model
|
tableView *dynamotableview.Model
|
||||||
itemView *dynamoitemview.Model
|
itemView *dynamoitemview.Model
|
||||||
mainView tea.Model
|
mainView tea.Model
|
||||||
keyMap *keybindings.ViewKeyBindings
|
keyMap *keybindings.ViewKeyBindings
|
||||||
|
keyBindingController *controllers.KeyBindingController
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewModel(
|
func NewModel(
|
||||||
|
@ -231,6 +232,7 @@ func NewModel(
|
||||||
itemView: div,
|
itemView: div,
|
||||||
mainView: mainView,
|
mainView: mainView,
|
||||||
keyMap: defaultKeyMap.View,
|
keyMap: defaultKeyMap.View,
|
||||||
|
keyBindingController: keyBindingController,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -285,6 +287,10 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
return m, events.SetTeaMessage(m.jobController.CancelRunningJob(m.promptToQuit))
|
return m, events.SetTeaMessage(m.jobController.CancelRunningJob(m.promptToQuit))
|
||||||
case key.Matches(msg, m.keyMap.Quit):
|
case key.Matches(msg, m.keyMap.Quit):
|
||||||
return m, m.promptToQuit
|
return m, m.promptToQuit
|
||||||
|
default:
|
||||||
|
if cmd := m.keyBindingController.LookupCustomBinding(msg.String()); cmd != nil {
|
||||||
|
return m, cmd
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue