Create a BoltItem interface and pass that into database items #1

Closed
dananglin wants to merge 15 commits from refactor/id-interface into main
8 changed files with 224 additions and 111 deletions
Showing only changes of commit 0e186be66b - Show all commits

1
.gitignore vendored
View file

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

View file

@ -1,12 +1,12 @@
package board package board
import ( import (
"bytes"
"encoding/gob"
"fmt" "fmt"
"sort" "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/database"
"forge.dananglin.me.uk/code/dananglin/pelican/internal/status"
bolt "go.etcd.io/bbolt" bolt "go.etcd.io/bbolt"
) )
@ -23,8 +23,15 @@ func LoadBoard(path string) (*bolt.DB, error) {
} }
if len(statusList) == 0 { if len(statusList) == 0 {
newStatusList := status.NewDefaultStatusList() newStatusList := defaultStatusList()
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 {
return nil, fmt.Errorf("unable to save the default status list to the database, %w", err) 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 return db, nil
} }
// ReadStatusList returns an ordered list of statuses from the database. // ReadStatusList returns the ordered list of statuses from the database.
func ReadStatusList(db *bolt.DB) ([]status.Status, error) { func ReadStatusList(db *bolt.DB) ([]Status, error) {
statuses, err := database.ReadStatusList(db) data, err := database.ReadAll(db, database.StatusBucket)
if err != nil { 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 return statuses, nil
} }
// TODO: Finish implementation. // TODO: Finish implementation.
func ReadStatus(db *bolt.DB) (status.Status, error) { func ReadStatus(db *bolt.DB) (Status, error) {
return status.Status{}, nil return Status{}, nil
} }
// TODO: Finish implementation. // TODO: Finish implementation.
@ -66,7 +89,7 @@ func DeleteStatus(db *bolt.DB) error {
// CreateCard creates a card in the database. // CreateCard creates a card in the database.
func CreateCard(db *bolt.DB, title, content string) error { func CreateCard(db *bolt.DB, title, content string) error {
statusList, err := database.ReadStatusList(db) statusList, err := ReadStatusList(db)
if err != nil { if err != nil {
return fmt.Errorf("unable to read the status list, %w", err) 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{} return statusListEmptyError{}
} }
card := card.Card{ card := Card{
ID: -1, ID: -1,
Title: title, Title: title,
Content: content, Content: content,
} }
cardID, err := database.WriteCard(db, card) cardID, err := database.Write(db, database.CardBucket, &card)
if err != nil { if err != nil {
return fmt.Errorf("unable to write card to the database, %w", err) 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 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)
// 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)
} }
return nil return nil
} }
// ReadCard returns a Card value from the database. // ReadCard returns a Card value from the database.
func ReadCard(db *bolt.DB, id int) (card.Card, error) { func ReadCard(db *bolt.DB, id int) (Card, error) {
c, err := database.ReadCard(db, id) data, err := database.Read(db, database.CardBucket, id)
if err != nil { 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. // UpdateCard modifies an existing card and saves the modification to the database.
func UpdateCard(db *bolt.DB, id int, title, content string) error { func UpdateCard(db *bolt.DB, id int, title, content string) error {
c, err := ReadCard(db, id) card, err := ReadCard(db, id)
if err != nil { if err != nil {
return err return err
} }
c.Title = title if len(title) > 0 {
c.Content = content 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) return fmt.Errorf("unable to write card to the database, %w", err)
} }

View file

@ -8,7 +8,6 @@ import (
"testing" "testing"
"forge.dananglin.me.uk/code/dananglin/pelican/internal/board" "forge.dananglin.me.uk/code/dananglin/pelican/internal/board"
"forge.dananglin.me.uk/code/dananglin/pelican/internal/card"
bolt "go.etcd.io/bbolt" bolt "go.etcd.io/bbolt"
) )
@ -32,17 +31,21 @@ func TestCardLifecycle(t *testing.T) {
_ = db.Close() _ = db.Close()
}() }()
cardTitle := "A test card." initialCardTitle := "A test card."
cardContent := "Ensure that this card is safely stored in the database." initialCardContent := "Ensure that this card is safely stored in the database."
expectedCardID := 1 expectedCardID := 1
testCreateCard(t, db, cardTitle, cardContent, expectedCardID) testCreateCard(t, db, initialCardTitle, initialCardContent, expectedCardID)
testReadCard(t, db, expectedCardID, cardTitle, cardContent) testReadCard(t, db, expectedCardID, initialCardTitle, initialCardContent)
newCardTitle := "Test card updated." modifiedCardTitle := "Test card updated."
newCardContent := "Ensure that this card is safely updated in the database." 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) { 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) t.Fatalf("Unable to read the modified test card, %s", err)
} }
want := card.Card{ want := board.Card{
ID: cardID, ID: cardID,
Title: newTitle, Title: newTitle,
Content: newContent, 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) { func projectRoot() (string, error) {
cwd, err := os.Getwd() cwd, err := os.Getwd()
if err != nil { 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. // Status represents the status of the Kanban board.
type Status struct { type Status struct {
@ -8,6 +8,20 @@ type Status struct {
Order int 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. // ByStatusOrder implements sort.Interface for []Status based on the Order field.
type ByStatusOrder []Status type ByStatusOrder []Status
@ -23,8 +37,8 @@ func (s ByStatusOrder) Less(i, j int) bool {
return s[i].Order < s[j].Order return s[i].Order < s[j].Order
} }
// NewDefaultStatusList creates the default list of statuses and saves the statuses to the database. // defaultStatusList returns the default list of statuses.
func NewDefaultStatusList() []Status { func defaultStatusList() []Status {
return []Status{ return []Status{
{ {
ID: -1, 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" "strconv"
"time" "time"
"forge.dananglin.me.uk/code/dananglin/pelican/internal/card"
"forge.dananglin.me.uk/code/dananglin/pelican/internal/status"
bolt "go.etcd.io/bbolt" bolt "go.etcd.io/bbolt"
) )
const ( const (
statusBucket string = "status" StatusBucket string = "status"
cardBucket string = "card" CardBucket string = "card"
) )
type BoltItem interface {
UpdateId(int)
Id() int
}
// OpenDatabase opens the database, at a given path, for reading and writing. // OpenDatabase opens the database, at a given path, for reading and writing.
// If the file does not exist it will be created. // If the file does not exist it will be created.
func OpenDatabase(path string) (*bolt.DB, error) { func OpenDatabase(path string) (*bolt.DB, error) {
@ -48,13 +51,13 @@ func OpenDatabase(path string) (*bolt.DB, error) {
return db, nil return db, nil
} }
// WriteStatusList saves one or more statuses to the status bucket. // WriteMany saves one or more statuses to the status bucket.
func WriteStatusList(db *bolt.DB, statuses []status.Status) error { func WriteMany(db *bolt.DB, bucketName string, items []BoltItem) error {
if len(statuses) == 0 { if len(items) == 0 {
return nil return nil
} }
bucket := []byte(statusBucket) bucket := []byte(bucketName)
err := db.Update(func(tx *bolt.Tx) error { err := db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket(bucket) b := tx.Bucket(bucket)
@ -63,24 +66,24 @@ func WriteStatusList(db *bolt.DB, statuses []status.Status) error {
return bucketNotExistError{bucket: string(bucket)} return bucketNotExistError{bucket: string(bucket)}
} }
for _, v := range statuses { for _, i := range items {
var err error var err error
if v.ID < 1 { if i.Id() < 1 {
var id uint64 var id uint64
if id, err = b.NextSequence(); err != nil { if id, err = b.NextSequence(); err != nil {
return fmt.Errorf("unable to generate ID, %w", err) return fmt.Errorf("unable to generate ID, %w", err)
} }
v.ID = int(id) i.UpdateId(int(id))
} }
buf := new(bytes.Buffer) buf := new(bytes.Buffer)
encoder := gob.NewEncoder(buf) 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) 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) 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 return nil
} }
// ReadStatusList retrieves all the statuses from the status bucket. // TODO: Create ReadMany
func ReadStatusList(db *bolt.DB) ([]status.Status, error) {
var statuses []status.Status
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 { err := db.View(func(tx *bolt.Tx) error {
b := tx.Bucket(bucket) b := tx.Bucket(bucket)
@ -108,14 +113,7 @@ func ReadStatusList(db *bolt.DB) ([]status.Status, error) {
} }
if err := b.ForEach(func(_, v []byte) error { if err := b.ForEach(func(_, v []byte) error {
var s status.Status output = append(output, v)
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)
return nil return nil
}); err != nil { }); err != nil {
@ -125,15 +123,15 @@ func ReadStatusList(db *bolt.DB) ([]status.Status, error) {
return nil return nil
}) })
if err != 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. // Write creates or updates a Bolt item to a specified bucket.
func WriteCard(db *bolt.DB, card card.Card) (int, error) { func Write(db *bolt.DB, bucketName string, item BoltItem) (int, error) {
bucket := []byte(cardBucket) bucket := []byte(bucketName)
err := db.Update(func(tx *bolt.Tx) error { err := db.Update(func(tx *bolt.Tx) error {
var err error var err error
@ -143,60 +141,54 @@ func WriteCard(db *bolt.DB, card card.Card) (int, error) {
return bucketNotExistError{bucket: string(bucket)} return bucketNotExistError{bucket: string(bucket)}
} }
if card.ID < 1 { if item.Id() < 1 {
var id uint64 var id uint64
if id, err = b.NextSequence(); err != nil { if id, err = b.NextSequence(); err != nil {
return fmt.Errorf("unable to generate an ID for the card, %w", err) 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) buf := new(bytes.Buffer)
encoder := gob.NewEncoder(buf) 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) 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 fmt.Errorf("unable to write the card to the bucket, %w", err)
} }
return nil return nil
}) })
if err != 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. // Read retrieves a Bolt item from a specified bucket and returns the data in bytes.
func ReadCard(db *bolt.DB, id int) (card.Card, error) { func Read(db *bolt.DB, bucketName string, id int) ([]byte, error) {
var card card.Card 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) b := tx.Bucket(bucket)
if b == nil { if b == nil {
return bucketNotExistError{bucket: string(bucket)} return bucketNotExistError{bucket: string(bucket)}
} }
buf := bytes.NewBuffer(b.Get([]byte(strconv.Itoa(id)))) data = 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)
}
return nil return nil
}) }); err != nil {
if err != nil { return []byte{}, fmt.Errorf("error while reading the Bolt item from the database, %w", err)
return card, fmt.Errorf("error while loading the card 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. // 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. // ensureBuckets ensures that the required buckets are created in the database.
func ensureBuckets(db *bolt.DB) error { func ensureBuckets(db *bolt.DB) error {
buckets := []string{statusBucket, cardBucket} buckets := []string{StatusBucket, CardBucket}
err := db.Update(func(tx *bolt.Tx) error { err := db.Update(func(tx *bolt.Tx) error {
for _, v := range buckets { for _, v := range buckets {

View file

@ -1,15 +1,16 @@
package database_test package database_test
import ( import (
"bytes"
"encoding/gob"
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"reflect" "reflect"
"testing" "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/database"
"forge.dananglin.me.uk/code/dananglin/pelican/internal/status"
bolt "go.etcd.io/bbolt" bolt "go.etcd.io/bbolt"
) )
@ -78,7 +79,7 @@ func TestWriteAndReadStatusList(t *testing.T) {
func testWriteStatusList(t *testing.T, db *bolt.DB) { func testWriteStatusList(t *testing.T, db *bolt.DB) {
t.Helper() t.Helper()
newStatusList := []status.Status{ newStatusList := []board.Status{
{ {
ID: -1, ID: -1,
Name: "Backlog", 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) 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) { func testReadStatusList(t *testing.T, db *bolt.DB) {
t.Helper() t.Helper()
got, err := database.ReadStatusList(db) data, err := database.ReadAll(db, database.StatusBucket)
if err != nil { if err != nil {
t.Fatalf("An error occurred whilst reading the modified status list from the database, %s", err) 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, ID: 1,
Name: "Backlog", Name: "Backlog",
@ -182,13 +205,13 @@ func TestReadAndWriteCard(t *testing.T) {
func testWriteCard(t *testing.T, db *bolt.DB) int { func testWriteCard(t *testing.T, db *bolt.DB) int {
t.Helper() t.Helper()
newCard := card.Card{ newCard := board.Card{
ID: -1, ID: -1,
Title: "A test task.", Title: "A test task.",
Content: "This task should be completed.", Content: "This task should be completed.",
} }
cardID, err := database.WriteCard(db, newCard) cardID, err := database.Write(db, database.CardBucket, &newCard)
if err != nil { if err != nil {
t.Fatalf("An error occurred whilst writing the card to the database, %s", err) 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) { func testReadCard(t *testing.T, db *bolt.DB, cardID int) {
t.Helper() t.Helper()
got, err := database.ReadCard(db, cardID) data, err := database.Read(db, database.CardBucket, cardID)
if err != nil { if err != nil {
t.Fatalf("An error occurred whilst loading the modified from the database, %s", err) 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, ID: 1,
Title: "A test task.", Title: "A test task.",
Content: "This task should be completed.", Content: "This task should be completed.",