Skip to content

Commit 2ab1ab7

Browse files
committed
initial commit
0 parents  commit 2ab1ab7

18 files changed

+755
-0
lines changed

.github/workflows/tests.yml

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
name: Tests
2+
3+
on: [push]
4+
5+
jobs:
6+
build:
7+
name: ${{matrix.go-version}} ${{matrix.os}}
8+
runs-on: ${{ matrix.os }}
9+
strategy:
10+
matrix:
11+
go-version: [1.17.x]
12+
os: [macos-latest, ubuntu-latest, windows-latest]
13+
steps:
14+
- uses: actions/checkout@v2
15+
16+
- name: Set up Go
17+
uses: actions/setup-go@v2
18+
with:
19+
go-version: 1.17.x
20+
21+
- name: Build
22+
run: go build -v ./...
23+
24+
- name: Test
25+
run: go test -race -cover ./...

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.vscode

book.go

+79
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package notes
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"io"
7+
)
8+
9+
// A Book allows for management, such as
10+
// inserting, updating, and deleting notes.
11+
type Book struct {
12+
Logger io.Writer // Logger for verbose logging
13+
14+
notes map[int]*Note
15+
curID int
16+
}
17+
18+
// Len returns the number of notes in the book.
19+
func (book *Book) Len() int {
20+
if book == nil {
21+
return 0
22+
}
23+
24+
return len(book.notes)
25+
}
26+
27+
// Restore restores the book from the provided reader.
28+
// The reader should contain a JSON-encoded book.
29+
func (book *Book) Restore(r io.Reader) error {
30+
if book == nil {
31+
return nil
32+
}
33+
34+
if r == nil {
35+
return fmt.Errorf("no reader provided")
36+
}
37+
38+
book.log("[RESTORE]\tstarting:\t(%d)\n", len(book.notes))
39+
40+
err := json.NewDecoder(r).Decode(book)
41+
42+
if err != nil && err != io.EOF {
43+
return fmt.Errorf("failed to decode book: %w", err)
44+
}
45+
46+
book.log("[RESTORE]\tfinished:\t(%d)\n", len(book.notes))
47+
48+
return nil
49+
}
50+
51+
// Backup backs up the book, in JSON, to the provided writer.
52+
func (book *Book) Backup(w io.Writer) error {
53+
if book == nil {
54+
return nil
55+
}
56+
57+
if w == nil {
58+
return fmt.Errorf("no writer provided")
59+
}
60+
61+
book.log("[BACKUP]\tstarting:\t(%d)\n", len(book.notes))
62+
63+
enc := json.NewEncoder(w)
64+
enc.SetIndent("", "\t")
65+
if err := enc.Encode(book); err != nil {
66+
return fmt.Errorf("failed to encode book: %w", err)
67+
}
68+
69+
book.log("[BACKUP]\tfinished:\t(%d)\n", len(book.notes))
70+
return nil
71+
}
72+
73+
func (book *Book) log(format string, a ...interface{}) {
74+
if book == nil || book.Logger == nil {
75+
return
76+
}
77+
78+
fmt.Fprintf(book.Logger, format, a...)
79+
}

book_json.go

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package notes
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
)
7+
8+
type bookJSON struct {
9+
CurID int `json:"cur_id,omitempty"`
10+
Notes map[int]*Note `json:"notes,omitempty"`
11+
}
12+
13+
// MarshalJSON returns the JSON encoding of the book.
14+
func (book *Book) MarshalJSON() ([]byte, error) {
15+
if book == nil {
16+
return nil, fmt.Errorf("book is nil")
17+
}
18+
19+
b := bookJSON{
20+
CurID: book.curID,
21+
Notes: book.notes,
22+
}
23+
24+
return json.MarshalIndent(b, "", " ")
25+
}
26+
27+
// UnmarshalJSON parses the JSON-encoded data and
28+
// stores the result in the book.
29+
func (book *Book) UnmarshalJSON(data []byte) error {
30+
if book == nil {
31+
return fmt.Errorf("book is nil")
32+
}
33+
34+
b := &bookJSON{}
35+
if err := json.Unmarshal(data, b); err != nil {
36+
return err
37+
}
38+
39+
book.curID = b.CurID
40+
book.notes = b.Notes
41+
42+
return nil
43+
}

book_test.go

+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package notes
2+
3+
import (
4+
"bytes"
5+
"fmt"
6+
"os"
7+
"testing"
8+
)
9+
10+
func Test_Book_Backup_Restore(t *testing.T) {
11+
t.Parallel()
12+
13+
b1 := &Book{}
14+
15+
if b1.Len() != 0 {
16+
t.Fatalf("book should be empty")
17+
}
18+
19+
const N = 5
20+
for i := 0; i < N; i++ {
21+
note := &Note{
22+
Body: "test!",
23+
}
24+
25+
err := b1.Insert(note)
26+
if err != nil {
27+
t.Fatal(err)
28+
}
29+
}
30+
31+
if b1.Len() != N {
32+
t.Fatalf("expected %d, got %d", N, b1.Len())
33+
}
34+
35+
bb := &bytes.Buffer{}
36+
err := b1.Backup(bb)
37+
if err != nil {
38+
t.Fatal(err)
39+
}
40+
41+
act := bb.Bytes()
42+
43+
b2 := &Book{
44+
Logger: os.Stdout,
45+
}
46+
47+
err = b2.Restore(bytes.NewReader(act))
48+
if err != nil {
49+
t.Fatal(err)
50+
}
51+
52+
if b2.Len() != N {
53+
t.Fatalf("expected %d, got %d", N, b2.Len())
54+
}
55+
56+
an := fmt.Sprint(b1.notes)
57+
en := fmt.Sprint(b2.notes)
58+
59+
if an != en {
60+
t.Fatalf("expected %s, got %s", an, en)
61+
}
62+
63+
if b1.curID != b2.curID {
64+
t.Fatalf("expected %d, got %d", b1.curID, b2.curID)
65+
}
66+
}

delete.go

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package notes
2+
3+
import "fmt"
4+
5+
// Delete removes the given notes from the book, if they exist.
6+
func (book *Book) Delete(ids ...int) error {
7+
if book == nil {
8+
return fmt.Errorf("book is nil")
9+
}
10+
11+
if len(ids) == 0 {
12+
return nil
13+
}
14+
15+
for _, id := range ids {
16+
book.log("[DELETE]\tby id:\t%d\n", id)
17+
18+
_, ok := book.notes[id]
19+
if !ok {
20+
err := ErrNotFound(id)
21+
book.log("[DELETE]\terror:\t%s\n", err)
22+
return err
23+
}
24+
delete(book.notes, id)
25+
26+
}
27+
28+
return nil
29+
}

delete_test.go

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package notes
2+
3+
import "testing"
4+
5+
func Test_Book_Delete(t *testing.T) {
6+
t.Parallel()
7+
8+
b := &Book{}
9+
10+
note := &Note{
11+
Body: "hello",
12+
}
13+
14+
err := b.Insert(note)
15+
if err != nil {
16+
t.Fatal(err)
17+
}
18+
19+
notes, err := b.Select(note.ID())
20+
if err != nil {
21+
t.Fatal(err)
22+
}
23+
24+
if len(notes) != 1 {
25+
t.Fatalf("expected 1 note, got %d", len(notes))
26+
}
27+
28+
n2 := notes[0]
29+
if n2.ID() != note.ID() {
30+
t.Fatalf("expected note id %d, got %d", note.ID(), n2.ID())
31+
}
32+
33+
err = b.Delete(note.ID())
34+
if err != nil {
35+
t.Fatal(err)
36+
}
37+
38+
}

errors.go

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package notes
2+
3+
import "fmt"
4+
5+
// ErrNotFound is returned when a note is not found.
6+
type ErrNotFound int
7+
8+
func (e ErrNotFound) Error() string {
9+
return fmt.Sprintf("note %d: not found", int(e))
10+
}

go.mod

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module github.com/gopherguides/notes
2+
3+
go 1.17

insert.go

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package notes
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
"time"
7+
)
8+
9+
// Insert adds the given notes to the book.
10+
func (book *Book) Insert(notes ...*Note) error {
11+
if book == nil {
12+
return fmt.Errorf("book is nil")
13+
}
14+
15+
for _, note := range notes {
16+
if !note.IsValid() {
17+
return fmt.Errorf("note is invalid")
18+
}
19+
20+
book.curID = book.curID + 1
21+
22+
note.id = book.curID
23+
24+
now := time.Now()
25+
note.createdAt = now
26+
note.updatedAt = now
27+
note.Body = strings.TrimSpace(note.Body)
28+
29+
book.log("[INSERT]\tinserting:\t%d\n", note.id)
30+
31+
if book.notes == nil {
32+
book.notes = map[int]*Note{}
33+
}
34+
35+
book.notes[note.id] = note
36+
37+
book.log("[INSERT]\tinserted:\t%d\n", note.id)
38+
}
39+
return nil
40+
}

insert_test.go

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package notes
2+
3+
import "testing"
4+
5+
func Test_Book_Insert(t *testing.T) {
6+
t.Parallel()
7+
8+
table := []struct {
9+
name string
10+
note *Note
11+
book *Book
12+
err bool
13+
}{
14+
{name: "all good", note: &Note{Body: "test"}, book: &Book{}},
15+
{name: "empty book", note: &Note{Body: "test"}, err: true},
16+
{name: "empty note and book", err: true},
17+
{name: "empty note", book: &Book{}, err: true},
18+
}
19+
20+
for _, tt := range table {
21+
t.Run(tt.name, func(t *testing.T) {
22+
err := tt.book.Insert(tt.note)
23+
24+
if tt.err {
25+
if err == nil {
26+
t.Errorf("expected error, got nil")
27+
}
28+
return
29+
}
30+
31+
if err != nil {
32+
t.Errorf("unexpected error: %v", err)
33+
}
34+
35+
exp := 1
36+
act := tt.book.Len()
37+
38+
if act != exp {
39+
t.Errorf("expected %v, got %v", exp, act)
40+
}
41+
42+
})
43+
}
44+
}

0 commit comments

Comments
 (0)