refactor: create BoltItem interface

Create a BoltItem interface which is used to make
the database fucntions more generic.

As part of this change, the Status and Card types
have migrated back into the board package.
This commit is contained in:
Dan Anglin 2021-09-23 21:14:35 +01:00
parent 3e5cd598d0
commit 0e186be66b
Signed by: dananglin
GPG key ID: 0C1D44CFBEE68638
8 changed files with 224 additions and 111 deletions

1
.gitignore vendored
View file

@ -1 +1,2 @@
/test/databases/*.db
/pelican

View file

@ -1,12 +1,12 @@
package board
import (
"bytes"
"encoding/gob"
"fmt"
"sort"
"forge.dananglin.me.uk/code/dananglin/pelican/internal/card"
"forge.dananglin.me.uk/code/dananglin/pelican/internal/database"
"forge.dananglin.me.uk/code/dananglin/pelican/internal/status"
bolt "go.etcd.io/bbolt"
)
@ -23,8 +23,15 @@ func LoadBoard(path string) (*bolt.DB, error) {
}
if len(statusList) == 0 {
newStatusList := status.NewDefaultStatusList()
if err := database.WriteStatusList(db, newStatusList); err != nil {
newStatusList := defaultStatusList()
boltItems := make([]database.BoltItem, len(newStatusList))
for i := range newStatusList {
boltItems[i] = &newStatusList[i]
}
if err := database.WriteMany(db, database.StatusBucket, boltItems); err != nil {
return nil, fmt.Errorf("unable to save the default status list to the database, %w", err)
}
}
@ -32,21 +39,37 @@ func LoadBoard(path string) (*bolt.DB, error) {
return db, nil
}
// ReadStatusList returns an ordered list of statuses from the database.
func ReadStatusList(db *bolt.DB) ([]status.Status, error) {
statuses, err := database.ReadStatusList(db)
// ReadStatusList returns the ordered list of statuses from the database.
func ReadStatusList(db *bolt.DB) ([]Status, error) {
data, err := database.ReadAll(db, database.StatusBucket)
if err != nil {
return statuses, fmt.Errorf("unable to read the status list, %w", err)
return []Status{}, fmt.Errorf("unable to read the status list, %w", err)
}
sort.Sort(status.ByStatusOrder(statuses))
statuses := make([]Status, len(data))
for i, d := range data {
buf := bytes.NewBuffer(d)
decoder := gob.NewDecoder(buf)
var s Status
if err := decoder.Decode(&s); err != nil {
return []Status{}, fmt.Errorf("unable to decode data, %w", err)
}
statuses[i] = s
}
sort.Sort(ByStatusOrder(statuses))
return statuses, nil
}
// TODO: Finish implementation.
func ReadStatus(db *bolt.DB) (status.Status, error) {
return status.Status{}, nil
func ReadStatus(db *bolt.DB) (Status, error) {
return Status{}, nil
}
// TODO: Finish implementation.
@ -66,7 +89,7 @@ func DeleteStatus(db *bolt.DB) error {
// CreateCard creates a card in the database.
func CreateCard(db *bolt.DB, title, content string) error {
statusList, err := database.ReadStatusList(db)
statusList, err := ReadStatusList(db)
if err != nil {
return fmt.Errorf("unable to read the status list, %w", err)
}
@ -75,52 +98,64 @@ func CreateCard(db *bolt.DB, title, content string) error {
return statusListEmptyError{}
}
card := card.Card{
card := Card{
ID: -1,
Title: title,
Content: content,
}
cardID, err := database.WriteCard(db, card)
cardID, err := database.Write(db, database.CardBucket, &card)
if err != nil {
return fmt.Errorf("unable to write card to the database, %w", err)
}
cardIDs := statusList[0].CardIds
initialStatus := statusList[0]
cardIDs = append(cardIDs, cardID)
initialStatus.AddCardID(cardID)
statusList[0].CardIds = cardIDs
// TODO: change the below to save a single status
if err := database.WriteStatusList(db, statusList); err != nil {
return fmt.Errorf("unable to write status list to the database, %w", err)
if _, err := database.Write(db, database.StatusBucket, &initialStatus); err != nil {
return fmt.Errorf("unable to write the %s status to the database, %w", initialStatus.Name, err)
}
return nil
}
// ReadCard returns a Card value from the database.
func ReadCard(db *bolt.DB, id int) (card.Card, error) {
c, err := database.ReadCard(db, id)
func ReadCard(db *bolt.DB, id int) (Card, error) {
data, err := database.Read(db, database.CardBucket, id)
if err != nil {
return card.Card{}, fmt.Errorf("unable to read card [%d] from the database, %w", id, err)
return Card{}, fmt.Errorf("unable to read card [%d] from the database, %w", id, err)
}
return c, nil
var card Card
buf := bytes.NewBuffer(data)
decoder := gob.NewDecoder(buf)
if err := decoder.Decode(&card); err != nil {
return Card{}, fmt.Errorf("unable to decode data, %w", err)
}
return card, nil
}
// UpdateCard modifies an existing card and saves the modification to the database.
func UpdateCard(db *bolt.DB, id int, title, content string) error {
c, err := ReadCard(db, id)
card, err := ReadCard(db, id)
if err != nil {
return err
}
c.Title = title
c.Content = content
if len(title) > 0 {
card.Title = title
}
if _, err := database.WriteCard(db, c); err != nil {
if len(content) > 0 {
card.Content = content
}
if _, err := database.Write(db, database.CardBucket, &card); err != nil {
return fmt.Errorf("unable to write card to the database, %w", err)
}

View file

@ -8,7 +8,6 @@ import (
"testing"
"forge.dananglin.me.uk/code/dananglin/pelican/internal/board"
"forge.dananglin.me.uk/code/dananglin/pelican/internal/card"
bolt "go.etcd.io/bbolt"
)
@ -32,17 +31,21 @@ func TestCardLifecycle(t *testing.T) {
_ = db.Close()
}()
cardTitle := "A test card."
cardContent := "Ensure that this card is safely stored in the database."
initialCardTitle := "A test card."
initialCardContent := "Ensure that this card is safely stored in the database."
expectedCardID := 1
testCreateCard(t, db, cardTitle, cardContent, expectedCardID)
testReadCard(t, db, expectedCardID, cardTitle, cardContent)
testCreateCard(t, db, initialCardTitle, initialCardContent, expectedCardID)
testReadCard(t, db, expectedCardID, initialCardTitle, initialCardContent)
newCardTitle := "Test card updated."
newCardContent := "Ensure that this card is safely updated in the database."
modifiedCardTitle := "Test card updated."
modifiedCardContent1 := "Ensure that this card is safely updated in the database."
testUpdateCard(t, db, expectedCardID, newCardTitle, newCardContent)
testUpdateCard(t, db, expectedCardID, modifiedCardTitle, modifiedCardContent1)
modifiedCardContent2 := "Updated card content only."
testUpdateCardContent(t, db, expectedCardID, modifiedCardTitle, modifiedCardContent2)
}
func testCreateCard(t *testing.T, db *bolt.DB, title, content string, wantID int) {
@ -107,7 +110,7 @@ func testUpdateCard(t *testing.T, db *bolt.DB, cardID int, newTitle, newContent
t.Fatalf("Unable to read the modified test card, %s", err)
}
want := card.Card{
want := board.Card{
ID: cardID,
Title: newTitle,
Content: newContent,
@ -120,6 +123,31 @@ func testUpdateCard(t *testing.T, db *bolt.DB, cardID int, newTitle, newContent
}
}
func testUpdateCardContent(t *testing.T, db *bolt.DB, cardID int, expectedTitle, newContent string) {
t.Helper()
if err := board.UpdateCard(db, cardID, "", newContent); err != nil {
t.Fatalf("Unable to update the test card, %s", err)
}
got, err := board.ReadCard(db, cardID)
if err != nil {
t.Fatalf("Unable to read the modified test card, %s", err)
}
want := board.Card{
ID: cardID,
Title: expectedTitle,
Content: newContent,
}
if !reflect.DeepEqual(got, want) {
t.Errorf("Unexpected card read from the database, want: %+v, got: %+v", want, got)
} else {
t.Logf("Expected card read from the database, got: %+v", got)
}
}
func projectRoot() (string, error) {
cwd, err := os.Getwd()
if err != nil {

18
internal/board/card.go Normal file
View file

@ -0,0 +1,18 @@
package board
// Card represents a card on a Kanban board.
type Card struct {
ID int
Title string
Content string
}
// UpdateId updates the ID of the Card value.
func (c *Card) UpdateId(id int) {
c.ID = id
}
// Id returns the ID of the Card value.
func (c *Card) Id() int {
return c.ID
}

View file

@ -1,4 +1,4 @@
package status
package board
// Status represents the status of the Kanban board.
type Status struct {
@ -8,6 +8,20 @@ type Status struct {
Order int
}
// UpdateId updates the ID of the Status value.
func (s *Status) UpdateId(id int) {
s.ID = id
}
// Id returns the ID of the Status value.
func (s *Status) Id() int {
return s.ID
}
func (s *Status) AddCardID(id int) {
s.CardIds = append(s.CardIds, id)
}
// ByStatusOrder implements sort.Interface for []Status based on the Order field.
type ByStatusOrder []Status
@ -23,8 +37,8 @@ func (s ByStatusOrder) Less(i, j int) bool {
return s[i].Order < s[j].Order
}
// NewDefaultStatusList creates the default list of statuses and saves the statuses to the database.
func NewDefaultStatusList() []Status {
// defaultStatusList returns the default list of statuses.
func defaultStatusList() []Status {
return []Status{
{
ID: -1,

View file

@ -1,8 +0,0 @@
package card
// Card represents a card on a Kanban board.
type Card struct {
ID int
Title string
Content string
}

View file

@ -10,16 +10,19 @@ import (
"strconv"
"time"
"forge.dananglin.me.uk/code/dananglin/pelican/internal/card"
"forge.dananglin.me.uk/code/dananglin/pelican/internal/status"
bolt "go.etcd.io/bbolt"
)
const (
statusBucket string = "status"
cardBucket string = "card"
StatusBucket string = "status"
CardBucket string = "card"
)
type BoltItem interface {
UpdateId(int)
Id() int
}
// OpenDatabase opens the database, at a given path, for reading and writing.
// If the file does not exist it will be created.
func OpenDatabase(path string) (*bolt.DB, error) {
@ -48,13 +51,13 @@ func OpenDatabase(path string) (*bolt.DB, error) {
return db, nil
}
// WriteStatusList saves one or more statuses to the status bucket.
func WriteStatusList(db *bolt.DB, statuses []status.Status) error {
if len(statuses) == 0 {
// WriteMany saves one or more statuses to the status bucket.
func WriteMany(db *bolt.DB, bucketName string, items []BoltItem) error {
if len(items) == 0 {
return nil
}
bucket := []byte(statusBucket)
bucket := []byte(bucketName)
err := db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket(bucket)
@ -63,24 +66,24 @@ func WriteStatusList(db *bolt.DB, statuses []status.Status) error {
return bucketNotExistError{bucket: string(bucket)}
}
for _, v := range statuses {
for _, i := range items {
var err error
if v.ID < 1 {
if i.Id() < 1 {
var id uint64
if id, err = b.NextSequence(); err != nil {
return fmt.Errorf("unable to generate ID, %w", err)
}
v.ID = int(id)
i.UpdateId(int(id))
}
buf := new(bytes.Buffer)
encoder := gob.NewEncoder(buf)
if err = encoder.Encode(v); err != nil {
if err = encoder.Encode(i); err != nil {
return fmt.Errorf("unable to encode data, %w", err)
}
if err = b.Put([]byte(strconv.Itoa(v.ID)), buf.Bytes()); err != nil {
if err = b.Put([]byte(strconv.Itoa(i.Id())), buf.Bytes()); err != nil {
return fmt.Errorf("unable to add the status to the bucket, %w", err)
}
}
@ -94,11 +97,13 @@ func WriteStatusList(db *bolt.DB, statuses []status.Status) error {
return nil
}
// ReadStatusList retrieves all the statuses from the status bucket.
func ReadStatusList(db *bolt.DB) ([]status.Status, error) {
var statuses []status.Status
// TODO: Create ReadMany
bucket := []byte(statusBucket)
// ReadAll retrieves all the statuses from the status bucket.
func ReadAll(db *bolt.DB, bucketName string) ([][]byte, error) {
bucket := []byte(bucketName)
var output [][]byte
err := db.View(func(tx *bolt.Tx) error {
b := tx.Bucket(bucket)
@ -108,14 +113,7 @@ func ReadStatusList(db *bolt.DB) ([]status.Status, error) {
}
if err := b.ForEach(func(_, v []byte) error {
var s status.Status
buf := bytes.NewBuffer(v)
decoder := gob.NewDecoder(buf)
if err := decoder.Decode(&s); err != nil {
return fmt.Errorf("unable to decode data, %w", err)
}
statuses = append(statuses, s)
output = append(output, v)
return nil
}); err != nil {
@ -125,15 +123,15 @@ func ReadStatusList(db *bolt.DB) ([]status.Status, error) {
return nil
})
if err != nil {
return statuses, fmt.Errorf("error while loading statuses from the database, %w", err)
return output, fmt.Errorf("error while loading statuses from the database, %w", err)
}
return statuses, nil
return output, nil
}
// WriteCard creates or updates a card to the card bucket in BoltDB.
func WriteCard(db *bolt.DB, card card.Card) (int, error) {
bucket := []byte(cardBucket)
// Write creates or updates a Bolt item to a specified bucket.
func Write(db *bolt.DB, bucketName string, item BoltItem) (int, error) {
bucket := []byte(bucketName)
err := db.Update(func(tx *bolt.Tx) error {
var err error
@ -143,60 +141,54 @@ func WriteCard(db *bolt.DB, card card.Card) (int, error) {
return bucketNotExistError{bucket: string(bucket)}
}
if card.ID < 1 {
if item.Id() < 1 {
var id uint64
if id, err = b.NextSequence(); err != nil {
return fmt.Errorf("unable to generate an ID for the card, %w", err)
}
card.ID = int(id)
item.UpdateId(int(id))
}
buf := new(bytes.Buffer)
encoder := gob.NewEncoder(buf)
if err = encoder.Encode(card); err != nil {
if err = encoder.Encode(item); err != nil {
return fmt.Errorf("unable to encode data, %w", err)
}
if err = b.Put([]byte(strconv.Itoa(card.ID)), buf.Bytes()); err != nil {
if err = b.Put([]byte(strconv.Itoa(item.Id())), buf.Bytes()); err != nil {
return fmt.Errorf("unable to write the card to the bucket, %w", err)
}
return nil
})
if err != nil {
return card.ID, fmt.Errorf("error while saving the card to the database, %w", err)
return 0, fmt.Errorf("error while saving the card to the database, %w", err)
}
return card.ID, nil
return item.Id(), nil
}
// ReadCard reads a card from the cards bucket in BoltDB.
func ReadCard(db *bolt.DB, id int) (card.Card, error) {
var card card.Card
// Read retrieves a Bolt item from a specified bucket and returns the data in bytes.
func Read(db *bolt.DB, bucketName string, id int) ([]byte, error) {
bucket := []byte(bucketName)
bucket := []byte(cardBucket)
var data []byte
err := db.View(func(tx *bolt.Tx) error {
if err := db.View(func(tx *bolt.Tx) error {
b := tx.Bucket(bucket)
if b == nil {
return bucketNotExistError{bucket: string(bucket)}
}
buf := bytes.NewBuffer(b.Get([]byte(strconv.Itoa(id))))
decoder := gob.NewDecoder(buf)
if err := decoder.Decode(&card); err != nil {
return fmt.Errorf("unable to decode card data, %w", err)
}
data = b.Get([]byte(strconv.Itoa(id)))
return nil
})
if err != nil {
return card, fmt.Errorf("error while loading the card from the database, %w", err)
}); err != nil {
return []byte{}, fmt.Errorf("error while reading the Bolt item from the database, %w", err)
}
return card, nil
return data, nil
}
// dbPath returns the path to the database file. If a path is given then that is returned. Otherwise the default path is returned.
@ -246,7 +238,7 @@ func mkDataDir(path string) error {
// ensureBuckets ensures that the required buckets are created in the database.
func ensureBuckets(db *bolt.DB) error {
buckets := []string{statusBucket, cardBucket}
buckets := []string{StatusBucket, CardBucket}
err := db.Update(func(tx *bolt.Tx) error {
for _, v := range buckets {

View file

@ -1,15 +1,16 @@
package database_test
import (
"bytes"
"encoding/gob"
"fmt"
"os"
"path/filepath"
"reflect"
"testing"
"forge.dananglin.me.uk/code/dananglin/pelican/internal/card"
"forge.dananglin.me.uk/code/dananglin/pelican/internal/board"
"forge.dananglin.me.uk/code/dananglin/pelican/internal/database"
"forge.dananglin.me.uk/code/dananglin/pelican/internal/status"
bolt "go.etcd.io/bbolt"
)
@ -78,7 +79,7 @@ func TestWriteAndReadStatusList(t *testing.T) {
func testWriteStatusList(t *testing.T, db *bolt.DB) {
t.Helper()
newStatusList := []status.Status{
newStatusList := []board.Status{
{
ID: -1,
Name: "Backlog",
@ -105,7 +106,13 @@ func testWriteStatusList(t *testing.T, db *bolt.DB) {
},
}
if err := database.WriteStatusList(db, newStatusList); err != nil {
boltItems := make([]database.BoltItem, len(newStatusList))
for i := range newStatusList {
boltItems[i] = &newStatusList[i]
}
if err := database.WriteMany(db, database.StatusBucket, boltItems); err != nil {
t.Fatalf("An error occurred whilst writing the initial status list to the database, %s", err)
}
}
@ -113,12 +120,28 @@ func testWriteStatusList(t *testing.T, db *bolt.DB) {
func testReadStatusList(t *testing.T, db *bolt.DB) {
t.Helper()
got, err := database.ReadStatusList(db)
data, err := database.ReadAll(db, database.StatusBucket)
if err != nil {
t.Fatalf("An error occurred whilst reading the modified status list from the database, %s", err)
}
want := []status.Status{
got := make([]board.Status, len(data))
for i, d := range data {
buf := bytes.NewBuffer(d)
decoder := gob.NewDecoder(buf)
var s board.Status
if err := decoder.Decode(&s); err != nil {
t.Fatalf("An error occurred whilst decoding data, %s", err)
}
got[i] = s
}
want := []board.Status{
{
ID: 1,
Name: "Backlog",
@ -182,13 +205,13 @@ func TestReadAndWriteCard(t *testing.T) {
func testWriteCard(t *testing.T, db *bolt.DB) int {
t.Helper()
newCard := card.Card{
newCard := board.Card{
ID: -1,
Title: "A test task.",
Content: "This task should be completed.",
}
cardID, err := database.WriteCard(db, newCard)
cardID, err := database.Write(db, database.CardBucket, &newCard)
if err != nil {
t.Fatalf("An error occurred whilst writing the card to the database, %s", err)
}
@ -199,12 +222,22 @@ func testWriteCard(t *testing.T, db *bolt.DB) int {
func testReadCard(t *testing.T, db *bolt.DB, cardID int) {
t.Helper()
got, err := database.ReadCard(db, cardID)
data, err := database.Read(db, database.CardBucket, cardID)
if err != nil {
t.Fatalf("An error occurred whilst loading the modified from the database, %s", err)
}
want := card.Card{
var got board.Card
buf := bytes.NewBuffer(data)
decoder := gob.NewDecoder(buf)
if err := decoder.Decode(&got); err != nil {
t.Fatalf("Unable to decode data, %s", err)
}
want := board.Card{
ID: 1,
Title: "A test task.",
Content: "This task should be completed.",