From f913b63c5815c3f64618874500a3584f28b4009d Mon Sep 17 00:00:00 2001 From: mappu Date: Sat, 8 Jun 2024 13:34:33 +1200 Subject: [PATCH] bolt: refactor extract to separate interface --- bolt.go | 178 ++++++++++++++++++++++++++++++++++++++++++++++ loadedDatabase.go | 21 ++++++ main.go | 158 ++-------------------------------------- 3 files changed, 205 insertions(+), 152 deletions(-) create mode 100644 bolt.go create mode 100644 loadedDatabase.go diff --git a/bolt.go b/bolt.go new file mode 100644 index 0000000..861581e --- /dev/null +++ b/bolt.go @@ -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 +} diff --git a/loadedDatabase.go b/loadedDatabase.go new file mode 100644 index 0000000..fe06b26 --- /dev/null +++ b/loadedDatabase.go @@ -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 +} diff --git a/main.go b/main.go index b4056a7..66c75d2 100644 --- a/main.go +++ b/main.go @@ -2,10 +2,8 @@ package main import ( "fmt" - "path/filepath" - "sort" + "os" "strings" - "time" "unicode/utf8" "unsafe" @@ -13,22 +11,8 @@ import ( "github.com/ying32/govcl/vcl" "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 { *vcl.TForm Menu *vcl.TMainMenu @@ -37,8 +21,7 @@ type TMainForm struct { propertiesBox *vcl.TMemo contentBox *vcl.TListView - dbs []loadedDatabase - arena []*navData // keepalive + dbs []loadedDatabase } var ( @@ -135,43 +118,7 @@ func (f *TMainForm) OnNavChange(sender vcl.IObject, node *vcl.TTreeNode) { } ndata := (*navData)(node.Data()) - - // 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) + ndata.ld.RenderForNav(f, ndata) // Handover to the database type's own renderer function } func formatUtf8(in []byte) string { @@ -182,73 +129,6 @@ func formatUtf8(in []byte) string { 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) { 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 - nextBucketNames, err := boltChildBucketNames(ndata.ld.db, ndata.bucketPath) + nextBucketNames, err := ndata.ld.NavChildren(ndata) if err != nil { vcl.ShowMessage(fmt.Sprintf("Failed to find child buckets under %q: %s", strings.Join(ndata.bucketPath, `/`), err.Error())) *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, bucketName) node.SetData(unsafe.Pointer(navData)) - f.arena = append(f.arena, navData) // keepalive + + ndata.ld.Keepalive(navData) } *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 -}