kanban

Kanban client in Gio
Log | Files | Refs | README | LICENSE

commit 5ebb59e9776b3fa17de96f8778dea5d42d3a2557
parent e1dc94831c592dd1ac5f5038020e743b969a42fd
Author: Jack Mordaunt <jackmordaunt@gmail.com>
Date:   Fri,  9 Apr 2021 21:08:42 +0800

fix: use raw boltdb as underlying database tech to simplify stack

Diffstat:
Mcmd/kanban/main.go | 3+--
Mcmd/kanban/ui.go | 28+++++++++++++++-------------
Mgo.mod | 3+--
Mgo.sum | 38++------------------------------------
Astorage/bolt/bolt.go | 166+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mstorage/lazy/lazy.go | 17++++++++++++++---
Mstorage/mem/mem.go | 14++++++++++++++
Mstorage/storage.go | 5+++++
Dstorage/storm/storm.go | 98-------------------------------------------------------------------------------
9 files changed, 218 insertions(+), 154 deletions(-)

diff --git a/cmd/kanban/main.go b/cmd/kanban/main.go @@ -6,14 +6,13 @@ import ( "os" "path/filepath" - "git.sr.ht/~jackmordaunt/kanban/storage/lazy" - "github.com/spf13/pflag" "gioui.org/font/gofont" "gioui.org/unit" "gioui.org/widget/material" "git.sr.ht/~jackmordaunt/kanban/storage" + "git.sr.ht/~jackmordaunt/kanban/storage/lazy" "git.sr.ht/~jackmordaunt/kanban/storage/mem" "gioui.org/app" diff --git a/cmd/kanban/ui.go b/cmd/kanban/ui.go @@ -98,18 +98,21 @@ type UI struct { // Loop runs the event loop until terminated. func (ui *UI) Loop() error { - var ( - ops op.Ops - events = ui.Window.Events() - ) - projects, err := ui.Storage.List() + count, err := ui.Storage.Count() if err != nil { - return fmt.Errorf("loading projects: %v", err) + return fmt.Errorf("counting projects: %w", err) + } + ui.Projects = make(Projects, count) + if err := ui.Storage.Load(ui.Projects); err != nil { + return fmt.Errorf("loading projects: %w", err) } - ui.Projects = projects if len(ui.Projects) > 0 { ui.Project = &ui.Projects[0] } + var ( + ops op.Ops + events = ui.Window.Events() + ) for event := range events { switch event := (event).(type) { case system.DestroyEvent: @@ -136,7 +139,7 @@ func (ui *UI) Shutdown() error { // Update state based on events. // -// TODO: investigate best way to handle errors. Some errors are for the user +// TODO(jfm): investigate best way to handle errors. Some errors are for the user // and others are for the devs. // Currently errors are just printed; not great for windowed applications. func (ui *UI) Update(gtx C) { @@ -294,11 +297,7 @@ func (ui *UI) layoutRail(gtx C) D { var ( rc []control.RailChild ) - projects, err := ui.Storage.List() - if err != nil { - log.Printf("error: loading projects: %v", err) - } - for _, p := range projects { + for _, p := range ui.Projects { p := p rc = append(rc, control.Destination(p.Name, func(gtx C) D { return layout.Stack{ @@ -401,6 +400,9 @@ func (ui *UI) layoutContent(gtx C) D { }.Layout( gtx, func() (panels []layout.FlexChild) { + if len(ui.Panels) == 0 { + return panels + } // @decouple this iteration relies on the coincidence that panels are ordered the same. for ii, stage := range ui.Project.Stages { stage := stage diff --git a/go.mod b/go.mod @@ -5,10 +5,9 @@ go 1.15 require ( gioui.org v0.0.0-20210210205757-7286b075e2fb gioui.org/x v0.0.0-20210120222453-b55819bc712b - github.com/asdine/storm/v3 v3.2.1 + github.com/boltdb/bolt v1.3.1 github.com/google/uuid v1.2.0 github.com/spf13/pflag v1.0.5 - go.etcd.io/bbolt v1.3.5 // indirect golang.org/x/exp v0.0.0-20210212053707-62dc52270d37 golang.org/x/image v0.0.0-20201208152932-35266b937fa6 // indirect golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c // indirect diff --git a/go.sum b/go.sum @@ -6,39 +6,14 @@ gioui.org v0.0.0-20210210205757-7286b075e2fb/go.mod h1:Y+uS7hHMvku1Q+ooaoq6fYD5B gioui.org/x v0.0.0-20210120222453-b55819bc712b h1:C7/1fHOBkcypkgWopz8Mf+eDJggTWQWlL315KjhDVDQ= gioui.org/x v0.0.0-20210120222453-b55819bc712b/go.mod h1:+CDNGkslHDIe1EK2Q4E0G98qRllCaQrL6qtjBhBjSAo= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/DataDog/zstd v1.4.1 h1:3oxKN3wbHibqx897utPC2LTQU4J+IHWWJO+glkAkpFM= -github.com/DataDog/zstd v1.4.1/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= -github.com/Sereal/Sereal v0.0.0-20190618215532-0b8ac451a863 h1:BRrxwOZBolJN4gIwvZMJY1tzqBvQgpaZiQRuIDD40jM= -github.com/Sereal/Sereal v0.0.0-20190618215532-0b8ac451a863/go.mod h1:D0JMgToj/WdxCgd30Kc1UcA9E+WdZoJqeVOuYW7iTBM= -github.com/asdine/storm/v3 v3.2.1 h1:I5AqhkPK6nBZ/qJXySdI7ot5BlXSZ7qvDY1zAn5ZJac= -github.com/asdine/storm/v3 v3.2.1/go.mod h1:LEpXwGt4pIqrE/XcTvCnZHT5MgZCV6Ub9q7yQzOFWr0= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4= +github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= -github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs= github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI= -github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= -go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= -go.etcd.io/bbolt v1.3.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0= -go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -63,21 +38,16 @@ golang.org/x/mod v0.1.1-0.20191209134235-331c550502dd/go.mod h1:s0Qsj1ACt9ePp/hM golang.org/x/mod v0.3.1-0.20200828183125-ce943fd02449/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191105084925-a882066a44e0 h1:QPlSTtPE2k6PZPasQUbzuK3p9JbS+vMXYVto8g/yrsg= -golang.org/x/net v0.0.0-20191105084925-a882066a44e0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c h1:VwygUrnw9jn88c4u8GD3rZQbqrP/tgas88tPUbBxQrk= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -89,7 +59,3 @@ golang.org/x/tools v0.0.0-20200117012304-6edc0a871e69/go.mod h1:TB2adYChydJhpapK golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM= -google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/storage/bolt/bolt.go b/storage/bolt/bolt.go @@ -0,0 +1,166 @@ +package bolt + +import ( + "encoding/json" + "fmt" + + "git.sr.ht/~jackmordaunt/kanban" + "git.sr.ht/~jackmordaunt/kanban/storage" + "github.com/boltdb/bolt" + "github.com/google/uuid" +) + +type Bucket []byte + +func (b Bucket) String() string { + return string(b) +} + +var ( + BucketProject Bucket = Bucket("Project") +) + +var _ storage.Storer = (*Storer)(nil) + +type Storer struct { + *bolt.DB +} + +func Open(path string) (*Storer, error) { + db, err := bolt.Open(path, 0660, nil) + if err != nil { + return nil, fmt.Errorf("opening database file: %w", err) + } + if err := db.Update(func(tx *bolt.Tx) error { + if _, err := tx.CreateBucketIfNotExists(BucketProject); err != nil { + return err + } + return nil + }); err != nil { + return nil, fmt.Errorf("initializing buckets: %w", err) + } + return &Storer{DB: db}, nil +} + +func (db *Storer) Create(p kanban.Project) error { + id, err := p.ID.MarshalBinary() + if err != nil { + return fmt.Errorf("serializing project ID: %w", err) + } + v, err := json.Marshal(p) + if err != nil { + return fmt.Errorf("serializing project: %w", err) + } + return db.Update(func(tx *bolt.Tx) error { + b := tx.Bucket(BucketProject) + if b == nil { + return fmt.Errorf("bucket not initialized: %s", BucketProject) + } + if b.Get(id) != nil { + return fmt.Errorf("project already exists for ID %q", p.ID) + } + return b.Put(id, v) + }) +} + +func (db *Storer) Save(projects ...kanban.Project) error { + return db.Update(func(tx *bolt.Tx) error { + for _, p := range projects { + if p.ID == uuid.Nil { + continue + } + id, err := p.ID.MarshalBinary() + if err != nil { + return fmt.Errorf("serializing project ID: %w", err) + } + v, err := json.Marshal(p) + if err != nil { + return fmt.Errorf("serializing project: %w", err) + } + if err := tx.Bucket(BucketProject).Put(id, v); err != nil { + return fmt.Errorf("updating project: %w", err) + } + } + return nil + }) +} + +func (db *Storer) Lookup(name string) (p kanban.Project, ok bool, err error) { + // @enhance: index by name + return p, ok, db.View(func(tx *bolt.Tx) error { + c := tx.Bucket(BucketProject).Cursor() + for k, v := c.First(); k != nil; k, v = c.Next() { + if err := json.Unmarshal(v, &p); err != nil { + return fmt.Errorf("deserializing project: %w", err) + } + if p.Name == name { + ok = true + break + } + } + return nil + }) +} + +func (db *Storer) Find(id uuid.UUID) (p kanban.Project, ok bool, err error) { + key, err := id.MarshalBinary() + if err != nil { + return p, false, fmt.Errorf("serializing id: %w", err) + } + return p, ok, db.View(func(tx *bolt.Tx) error { + if err := json.Unmarshal(tx.Bucket(BucketProject).Get(key), &p); err != nil { + return fmt.Errorf("deserializing project: %w", err) + } + ok = true + return nil + }) +} + +func (db *Storer) List() (list []kanban.Project, err error) { + return list, db.View(func(tx *bolt.Tx) error { + c := tx.Bucket(BucketProject).Cursor() + for k, v := c.First(); k != nil; k, v = c.Next() { + var p kanban.Project + if err := json.Unmarshal(v, &p); err != nil { + return fmt.Errorf("deserializing project: %w", err) + } + list = append(list, p) + } + return nil + }) +} + +func (db *Storer) Load(projects []kanban.Project) error { + if len(projects) > 0 { + if projects[0].ID == uuid.Nil { + list, err := db.List() + if err != nil { + return err + } + copy(projects, list) + return nil + } + } + return db.View(func(tx *bolt.Tx) error { + for ii, p := range projects { + id, err := p.ID.MarshalBinary() + if err != nil { + return fmt.Errorf("serializing project ID: %w", err) + } + if err := json.Unmarshal(tx.Bucket(BucketProject).Get(id), &projects[ii]); err != nil { + return fmt.Errorf("deserializing project: %w", err) + } + } + return nil + }) +} + +func (db *Storer) Count() (count int, err error) { + return count, db.View(func(tx *bolt.Tx) error { + c := tx.Bucket(BucketProject).Cursor() + for k, _ := c.First(); k != nil; k, _ = c.Next() { + count++ + } + return nil + }) +} diff --git a/storage/lazy/lazy.go b/storage/lazy/lazy.go @@ -8,8 +8,9 @@ import ( "git.sr.ht/~jackmordaunt/kanban" "git.sr.ht/~jackmordaunt/kanban/storage" + "git.sr.ht/~jackmordaunt/kanban/storage/bolt" "git.sr.ht/~jackmordaunt/kanban/storage/mem" - "git.sr.ht/~jackmordaunt/kanban/storage/storm" + "github.com/google/uuid" ) var _ storage.Storer = (*Storer)(nil) @@ -17,13 +18,13 @@ var _ storage.Storer = (*Storer)(nil) // Storer writes to disk when a change has been detected. type Storer struct { Cache *mem.Storer - Disk *storm.Storer + Disk *bolt.Storer } // Open a lazy storer, initializing the underlying database at the path // specified. func Open(path string) (*Storer, error) { - disk, err := storm.Open(path) + disk, err := bolt.Open(path) if err != nil { return nil, err } @@ -71,6 +72,12 @@ func (s *Storer) Lookup(name string) (kanban.Project, bool, error) { return s.Cache.Lookup(name) } +// Load a project by ID. +// Bool indicates whether a project exists for that ID. +func (s *Storer) Find(id uuid.UUID) (kanban.Project, bool, error) { + return s.Cache.Find(id) +} + // List projects. func (s *Storer) List() ([]kanban.Project, error) { return s.Disk.List() @@ -109,3 +116,7 @@ func (s *Storer) Load(projects []kanban.Project) error { func (s *Storer) Close() error { return s.Disk.DB.Close() } + +func (s *Storer) Count() (int, error) { + return s.Cache.Count() +} diff --git a/storage/mem/mem.go b/storage/mem/mem.go @@ -4,6 +4,7 @@ import ( "fmt" "git.sr.ht/~jackmordaunt/kanban/storage" + "github.com/google/uuid" "git.sr.ht/~jackmordaunt/kanban" ) @@ -49,6 +50,19 @@ func (s *Storer) Lookup(name string) (kanban.Project, bool, error) { return kanban.Project{}, false, nil } +func (s *Storer) Find(id uuid.UUID) (kanban.Project, bool, error) { + for _, p := range s.Data { + if p.ID == id { + return s.Data[p.Name], true, nil + } + } + return kanban.Project{}, false, nil +} + +func (s *Storer) Count() (int, error) { + return len(s.Data), nil +} + func (s *Storer) List() (list []kanban.Project, err error) { for _, name := range s.Order { if p, ok := s.Data[name]; ok { diff --git a/storage/storage.go b/storage/storage.go @@ -4,6 +4,7 @@ package storage import ( "git.sr.ht/~jackmordaunt/kanban" + "github.com/google/uuid" ) // Storer persists Project entities. @@ -18,6 +19,10 @@ type Storer interface { Load([]kanban.Project) error // Lookup a Project by name. Lookup(name string) (kanban.Project, bool, error) + // Find a Project by ID. + Find(id uuid.UUID) (kanban.Project, bool, error) // List all existing Projects. List() ([]kanban.Project, error) + // Count returns the number of projects that exist in the store. + Count() (int, error) } diff --git a/storage/storm/storm.go b/storage/storm/storm.go @@ -1,98 +0,0 @@ -package storm - -import ( - "errors" - "fmt" - "log" - - "git.sr.ht/~jackmordaunt/kanban" - "git.sr.ht/~jackmordaunt/kanban/storage" - - "github.com/asdine/storm/v3" -) - -var _ storage.Storer = (*Storer)(nil) - -// Storer implements Project storage using storm db. -type Storer struct { - DB *storm.DB -} - -// Schema is a database representation of a kanban.Project. -type Schema struct { - ID string `storm:"id"` - Project kanban.Project -} - -// Open a database handle using the file specified by path. -func Open(path string) (*Storer, error) { - db, err := storm.Open(path) - if err != nil { - return nil, fmt.Errorf("opening data file: %w", err) - } - if err := db.Init(&Schema{}); err != nil { - return nil, fmt.Errorf("initialising schema: %v", err) - } - if err != nil { - log.Fatalf("error: initializing data: %v", err) - } - return &Storer{DB: db}, nil - -} - -func (s *Storer) Create(p kanban.Project) error { - return s.DB.Save(&Schema{ID: p.Name, Project: p}) -} - -func (s *Storer) Save(projects ...kanban.Project) error { - for _, p := range projects { - p := p - if err := s.DB.Update(&Schema{ID: p.Name, Project: p}); err != nil { - return err - } - } - return nil -} - -func (s *Storer) Lookup(name string) (kanban.Project, bool, error) { - var schema Schema - if err := s.DB.One("ID", name, &schema); err != nil { - if errors.Is(err, storm.ErrNotFound) { - return schema.Project, false, nil - } else { - return schema.Project, false, err - } - } - return schema.Project, true, nil -} - -func (s *Storer) List() (list []kanban.Project, err error) { - var ( - projects []Schema - ) - if err := s.DB.All(&projects); err != nil { - return nil, fmt.Errorf("loading projects: %v", err) - } - for _, p := range projects { - p := p - list = append(list, p.Project) - } - return list, nil -} - -func (s *Storer) Load(projects []kanban.Project) error { - for ii := range projects { - var ( - name = projects[ii].Name - schema = Schema{ - ID: name, - Project: projects[ii], - } - ) - if err := s.DB.One("ID", name, &schema); err != nil { - return err - } - projects[ii] = schema.Project - } - return nil -}