bolt: refactor extract to separate interface

This commit is contained in:
mappu 2024-06-08 13:34:33 +12:00
parent d97c8872de
commit f913b63c58
3 changed files with 205 additions and 152 deletions

178
bolt.go Normal file
View File

@ -0,0 +1,178 @@
package main
import (
"fmt"
"path/filepath"
"sort"
"strings"
"time"
"unsafe"
"github.com/ying32/govcl/vcl"
"go.etcd.io/bbolt"
)
type boltLoadedDatabase struct {
displayName string
path string
db *bbolt.DB
nav *vcl.TTreeNode
arena []*navData // keepalive
}
func (ld *boltLoadedDatabase) DisplayName() string {
return ld.displayName
}
func (ld *boltLoadedDatabase) RootElement() *vcl.TTreeNode {
return ld.nav
}
func (ld *boltLoadedDatabase) Keepalive(ndata *navData) {
ld.arena = append(ld.arena, ndata)
}
func (ld *boltLoadedDatabase) RenderForNav(f *TMainForm, ndata *navData) {
// Load properties
bucketDisplayName := strings.Join(ndata.bucketPath, `/`)
content := fmt.Sprintf("Selected database: %#v\n\n\nSelected bucket: %q\n", ld.db.Stats(), bucketDisplayName)
f.propertiesBox.SetText(content)
// Load data
f.contentBox.SetEnabled(false)
f.contentBox.Clear()
err := ld.db.View(func(tx *bbolt.Tx) error {
b := boltTargetBucket(tx, ndata.bucketPath)
if b == nil {
// no such bucket
return nil
}
// Valid
f.contentBox.Clear()
c := b.Cursor()
for k, v := c.First(); k != nil; k, v = c.Next() {
dataEntry := f.contentBox.Items().Add()
dataEntry.SetCaption(formatUtf8(k))
dataEntry.SubItems().Add(formatUtf8(v))
}
return nil
})
if err != nil {
vcl.ShowMessage(fmt.Sprintf("Failed to load data for bucket %q: %s", bucketDisplayName, err.Error()))
return
}
// Valid
f.contentBox.SetEnabled(true)
}
func (ld *boltLoadedDatabase) NavChildren(ndata *navData) ([]string, error) {
// In the bolt implementation, the nav is a recursive tree of child buckets
return boltChildBucketNames(ld.db, ndata.bucketPath)
}
var _ loadedDatabase = &boltLoadedDatabase{} // interface assertion
//
func (f *TMainForm) addDatabaseFromFile(path string) {
// TODO load in background thread to stop blocking the UI
db, err := bbolt.Open(path, 0644, &bbolt.Options{Timeout: 1 * time.Second})
if err != nil {
vcl.ShowMessage(fmt.Sprintf("Failed to load database '%s': %s", path, err.Error()))
return
}
ld := &boltLoadedDatabase{
path: path,
displayName: filepath.Base(path),
db: db,
}
ld.nav = f.Buckets.Items().Add(nil, ld.displayName)
ld.nav.SetHasChildren(true) // dynamically populate in OnNavExpanding
navData := &navData{
ld: ld,
childrenLoaded: false, // will be loaded dynamically
bucketPath: []string{}, // empty = root
}
ld.nav.SetData(unsafe.Pointer(navData))
f.dbs = append(f.dbs, ld)
ld.Keepalive(navData)
}
func boltTargetBucket(tx *bbolt.Tx, path []string) *bbolt.Bucket {
// If we are already deep in buckets, go directly there to find children
if len(path) == 0 {
return nil
}
b := tx.Bucket([]byte(path[0]))
if b == nil {
return nil // unexpectedly missing
}
for i := 1; i < len(path); i += 1 {
b = b.Bucket([]byte(path[i]))
if b == nil {
return nil // unexpectedly missing
}
}
return b // OK
}
func boltChildBucketNames(db *bbolt.DB, path []string) ([]string, error) {
var nextBucketNames []string
err := db.View(func(tx *bbolt.Tx) error {
// If we are already deep in buckets, go directly there to find children
if len(path) > 0 {
b := tx.Bucket([]byte(path[0]))
if b == nil {
return fmt.Errorf("Unexpected missing root bucket %q", path[0])
}
for i := 1; i < len(path); i += 1 {
b = b.Bucket([]byte(path[i]))
if b == nil {
return fmt.Errorf("Unexpected missing bucket %q", strings.Join(path[0:i], `/`))
}
}
// Find child buckets of this bucket
b.ForEachBucket(func(bucketName []byte) error {
nextBucketNames = append(nextBucketNames, string(bucketName))
return nil
})
} else {
// Find root bucket names
return tx.ForEach(func(bucketName []byte, _ *bbolt.Bucket) error {
nextBucketNames = append(nextBucketNames, string(bucketName))
return nil
})
}
// OK
return nil
})
if err != nil {
return nil, err
}
sort.Strings(nextBucketNames)
return nextBucketNames, nil
}

21
loadedDatabase.go Normal file
View File

@ -0,0 +1,21 @@
package main
import (
"github.com/ying32/govcl/vcl"
)
// loadedDatabase is a DB-agnostic interface for each loaded database.
type loadedDatabase interface {
DisplayName() string
RootElement() *vcl.TTreeNode
RenderForNav(f *TMainForm, ndata *navData)
NavChildren(ndata *navData) ([]string, error)
Keepalive(ndata *navData)
}
// navData is the .Data() pointer for each TTreeNode in the left-hand tree.
type navData struct {
ld loadedDatabase
childrenLoaded bool
bucketPath []string
}

158
main.go
View File

@ -2,10 +2,8 @@ package main
import ( import (
"fmt" "fmt"
"path/filepath" "os"
"sort"
"strings" "strings"
"time"
"unicode/utf8" "unicode/utf8"
"unsafe" "unsafe"
@ -13,22 +11,8 @@ import (
"github.com/ying32/govcl/vcl" "github.com/ying32/govcl/vcl"
"github.com/ying32/govcl/vcl/types" "github.com/ying32/govcl/vcl/types"
"go.etcd.io/bbolt"
) )
type loadedDatabase struct {
displayName string
path string
db *bbolt.DB
nav *vcl.TTreeNode
}
type navData struct {
ld *loadedDatabase
childrenLoaded bool
bucketPath []string
}
type TMainForm struct { type TMainForm struct {
*vcl.TForm *vcl.TForm
Menu *vcl.TMainMenu Menu *vcl.TMainMenu
@ -37,8 +21,7 @@ type TMainForm struct {
propertiesBox *vcl.TMemo propertiesBox *vcl.TMemo
contentBox *vcl.TListView contentBox *vcl.TListView
dbs []loadedDatabase dbs []loadedDatabase
arena []*navData // keepalive
} }
var ( var (
@ -135,43 +118,7 @@ func (f *TMainForm) OnNavChange(sender vcl.IObject, node *vcl.TTreeNode) {
} }
ndata := (*navData)(node.Data()) ndata := (*navData)(node.Data())
ndata.ld.RenderForNav(f, ndata) // Handover to the database type's own renderer function
// Load properties
bucketDisplayName := strings.Join(ndata.bucketPath, `/`)
content := fmt.Sprintf("Selected database: %#v\n\n\nSelected bucket: %q\n", ndata.ld.db.Stats(), bucketDisplayName)
f.propertiesBox.SetText(content)
// Load data
f.contentBox.SetEnabled(false)
f.contentBox.Clear()
err := ndata.ld.db.View(func(tx *bbolt.Tx) error {
b := boltTargetBucket(tx, ndata.bucketPath)
if b == nil {
// no such bucket
return nil
}
// Valid
f.contentBox.Clear()
c := b.Cursor()
for k, v := c.First(); k != nil; k, v = c.Next() {
dataEntry := f.contentBox.Items().Add()
dataEntry.SetCaption(formatUtf8(k))
dataEntry.SubItems().Add(formatUtf8(v))
}
return nil
})
if err != nil {
vcl.ShowMessage(fmt.Sprintf("Failed to load data for bucket %q: %s", bucketDisplayName, err.Error()))
return
}
// Valid
f.contentBox.SetEnabled(true)
} }
func formatUtf8(in []byte) string { func formatUtf8(in []byte) string {
@ -182,73 +129,6 @@ func formatUtf8(in []byte) string {
return string(in) return string(in)
} }
func boltTargetBucket(tx *bbolt.Tx, path []string) *bbolt.Bucket {
// If we are already deep in buckets, go directly there to find children
if len(path) == 0 {
return nil
}
b := tx.Bucket([]byte(path[0]))
if b == nil {
return nil // unexpectedly missing
}
for i := 1; i < len(path); i += 1 {
b = b.Bucket([]byte(path[i]))
if b == nil {
return nil // unexpectedly missing
}
}
return b // OK
}
func boltChildBucketNames(db *bbolt.DB, path []string) ([]string, error) {
var nextBucketNames []string
err := db.View(func(tx *bbolt.Tx) error {
// If we are already deep in buckets, go directly there to find children
if len(path) > 0 {
b := tx.Bucket([]byte(path[0]))
if b == nil {
return fmt.Errorf("Unexpected missing root bucket %q", path[0])
}
for i := 1; i < len(path); i += 1 {
b = b.Bucket([]byte(path[i]))
if b == nil {
return fmt.Errorf("Unexpected missing bucket %q", strings.Join(path[0:i], `/`))
}
}
// Find child buckets of this bucket
b.ForEachBucket(func(bucketName []byte) error {
nextBucketNames = append(nextBucketNames, string(bucketName))
return nil
})
} else {
// Find root bucket names
return tx.ForEach(func(bucketName []byte, _ *bbolt.Bucket) error {
nextBucketNames = append(nextBucketNames, string(bucketName))
return nil
})
}
// OK
return nil
})
if err != nil {
return nil, err
}
sort.Strings(nextBucketNames)
return nextBucketNames, nil
}
func (f *TMainForm) OnNavExpanding(sender vcl.IObject, node *vcl.TTreeNode, allowExpansion *bool) { func (f *TMainForm) OnNavExpanding(sender vcl.IObject, node *vcl.TTreeNode, allowExpansion *bool) {
if node.Data() == nil { if node.Data() == nil {
@ -264,7 +144,7 @@ func (f *TMainForm) OnNavExpanding(sender vcl.IObject, node *vcl.TTreeNode, allo
} }
// Find the child buckets from this point under the element // Find the child buckets from this point under the element
nextBucketNames, err := boltChildBucketNames(ndata.ld.db, ndata.bucketPath) nextBucketNames, err := ndata.ld.NavChildren(ndata)
if err != nil { if err != nil {
vcl.ShowMessage(fmt.Sprintf("Failed to find child buckets under %q: %s", strings.Join(ndata.bucketPath, `/`), err.Error())) vcl.ShowMessage(fmt.Sprintf("Failed to find child buckets under %q: %s", strings.Join(ndata.bucketPath, `/`), err.Error()))
*allowExpansion = false *allowExpansion = false
@ -292,36 +172,10 @@ func (f *TMainForm) OnNavExpanding(sender vcl.IObject, node *vcl.TTreeNode, allo
navData.bucketPath = append(navData.bucketPath, ndata.bucketPath...) navData.bucketPath = append(navData.bucketPath, ndata.bucketPath...)
navData.bucketPath = append(navData.bucketPath, bucketName) navData.bucketPath = append(navData.bucketPath, bucketName)
node.SetData(unsafe.Pointer(navData)) node.SetData(unsafe.Pointer(navData))
f.arena = append(f.arena, navData) // keepalive
ndata.ld.Keepalive(navData)
} }
*allowExpansion = true *allowExpansion = true
} }
} }
func (f *TMainForm) addDatabaseFromFile(path string) {
// TODO load in background thread to stop blocking the UI
db, err := bbolt.Open(path, 0644, &bbolt.Options{Timeout: 1 * time.Second})
if err != nil {
vcl.ShowMessage(fmt.Sprintf("Failed to load database '%s': %s", path, err.Error()))
return
}
entry := loadedDatabase{
path: path,
displayName: filepath.Base(path),
db: db,
}
entry.nav = f.Buckets.Items().Add(nil, entry.displayName)
entry.nav.SetHasChildren(true) // dynamically populate in OnNavExpanding
navData := &navData{
ld: &entry,
childrenLoaded: false, // will be loaded dynamically
bucketPath: []string{}, // empty = root
}
entry.nav.SetData(unsafe.Pointer(navData))
f.dbs = append(f.dbs, entry)
f.arena = append(f.arena, navData) // keepalive
}