From 0e186be66b237312331d373feccdf8a44c6a3393 Mon Sep 17 00:00:00 2001 From: Dan Anglin Date: Thu, 23 Sep 2021 21:14:35 +0100 Subject: [PATCH] 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. --- .gitignore | 1 + internal/board/board.go | 93 ++++++++++++++++++--------- internal/board/board_test.go | 46 ++++++++++--- internal/board/card.go | 18 ++++++ internal/{status => board}/status.go | 20 +++++- internal/card/card.go | 8 --- internal/database/database.go | 96 +++++++++++++--------------- internal/database/database_test.go | 53 ++++++++++++--- 8 files changed, 224 insertions(+), 111 deletions(-) create mode 100644 internal/board/card.go rename internal/{status => board}/status.go (63%) delete mode 100644 internal/card/card.go diff --git a/.gitignore b/.gitignore index 9cfd77d..a8ad0d7 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /test/databases/*.db +/pelican diff --git a/internal/board/board.go b/internal/board/board.go index 473f3a1..2bcbeec 100644 --- a/internal/board/board.go +++ b/internal/board/board.go @@ -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) } diff --git a/internal/board/board_test.go b/internal/board/board_test.go index f4b61ba..859d6cd 100644 --- a/internal/board/board_test.go +++ b/internal/board/board_test.go @@ -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 { diff --git a/internal/board/card.go b/internal/board/card.go new file mode 100644 index 0000000..91b7f9e --- /dev/null +++ b/internal/board/card.go @@ -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 +} diff --git a/internal/status/status.go b/internal/board/status.go similarity index 63% rename from internal/status/status.go rename to internal/board/status.go index 7e0b93b..546a428 100644 --- a/internal/status/status.go +++ b/internal/board/status.go @@ -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, diff --git a/internal/card/card.go b/internal/card/card.go deleted file mode 100644 index 3dbdd53..0000000 --- a/internal/card/card.go +++ /dev/null @@ -1,8 +0,0 @@ -package card - -// Card represents a card on a Kanban board. -type Card struct { - ID int - Title string - Content string -} diff --git a/internal/database/database.go b/internal/database/database.go index 7842e96..21d87e0 100644 --- a/internal/database/database.go +++ b/internal/database/database.go @@ -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 { diff --git a/internal/database/database_test.go b/internal/database/database_test.go index f16afce..1789c71 100644 --- a/internal/database/database_test.go +++ b/internal/database/database_test.go @@ -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.",