Skip to content

Commit 502810e

Browse files
committedMar 30, 2021
Add todo service layer
1 parent 6597ee1 commit 502810e

File tree

5 files changed

+335
-0
lines changed

5 files changed

+335
-0
lines changed
 

‎README.md

+2
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
# go-todo
2+
3+
This repo contains a basic Todo application and is mostly to provide a Go coding sample.

‎go.mod

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
module github.com/MarkMoudy/go-todo
2+
3+
go 1.15
4+
5+
require github.com/google/go-cmp v0.5.5

‎go.sum

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
2+
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
3+
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

‎todo/service.go

+137
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
// package todo provides an API to store and manage Todos.
2+
package todo
3+
4+
import (
5+
"context"
6+
"errors"
7+
"sort"
8+
"sync"
9+
)
10+
11+
type Service interface {
12+
// Todo fetches a single Todo using the given 'id'.
13+
Todo(ctx context.Context, id int) (Todo, error)
14+
15+
// Todos fetches all Todos.
16+
Todos(ctx context.Context) ([]Todo, error)
17+
18+
// CreateTodo creates a new Todo with the given 'description'. All new Todo
19+
// items are initially marked as incomplete.
20+
CreateTodo(ctx context.Context, description string) error
21+
22+
// UpdateTodo modifies an existing Todo.
23+
UpdateTodo(ctx context.Context, id int, desc string) error
24+
25+
// SetCompletedStatus sets a Todo item's completed status.
26+
SetCompletedStatus(ctx context.Context, id int, completed bool) error
27+
}
28+
29+
// Todo represents a Todo task.
30+
type Todo struct {
31+
ID int
32+
Description string
33+
Completed bool
34+
}
35+
36+
type inmemService struct {
37+
nextID int
38+
39+
todos map[int]Todo
40+
mu sync.Mutex
41+
}
42+
43+
// NewInmemService returns an in memory implementation of Service.
44+
func NewInmemService() Service {
45+
return &inmemService{
46+
nextID: 1,
47+
todos: make(map[int]Todo),
48+
}
49+
}
50+
51+
// Errors returned by Service.
52+
var (
53+
ErrInvalidID = errors.New("invalid id, must be non-negative")
54+
ErrTodoNotFound = errors.New("todo not found")
55+
)
56+
57+
// Todo implements Service.
58+
func (s *inmemService) Todo(ctx context.Context, id int) (Todo, error) {
59+
if id < 0 {
60+
return Todo{}, ErrInvalidID
61+
}
62+
63+
s.mu.Lock()
64+
defer s.mu.Unlock()
65+
if todo, ok := s.todos[id]; ok {
66+
return todo, nil
67+
}
68+
69+
return Todo{}, nil
70+
}
71+
72+
// Todos implements Service.
73+
func (s *inmemService) Todos(ctx context.Context) ([]Todo, error) {
74+
s.mu.Lock()
75+
defer s.mu.Unlock()
76+
77+
var todos []Todo
78+
for _, v := range s.todos {
79+
todos = append(todos, v)
80+
}
81+
82+
sort.Slice(todos, func(i, j int) bool {
83+
return todos[i].ID < todos[j].ID
84+
})
85+
return todos, nil
86+
}
87+
88+
// CreateTodo implements Service.
89+
func (s *inmemService) CreateTodo(ctx context.Context, desc string) error {
90+
s.mu.Lock()
91+
defer s.mu.Unlock()
92+
todo := Todo{
93+
ID: s.generateID(),
94+
Description: desc,
95+
}
96+
97+
s.todos[todo.ID] = todo
98+
return nil
99+
}
100+
101+
func (s *inmemService) generateID() int {
102+
defer func() { s.nextID++ }()
103+
return s.nextID
104+
}
105+
106+
// UpdateTodo implements Service.
107+
func (s *inmemService) UpdateTodo(ctx context.Context, id int, desc string) error {
108+
s.mu.Lock()
109+
defer s.mu.Unlock()
110+
111+
todo, ok := s.todos[id]
112+
if !ok {
113+
return ErrTodoNotFound
114+
}
115+
116+
todo.Description = desc
117+
s.todos[id] = todo
118+
119+
return nil
120+
}
121+
122+
// SetCompletedStatus implements Service.
123+
func (s *inmemService) SetCompletedStatus(ctx context.Context, id int, completed bool) error {
124+
s.mu.Lock()
125+
defer s.mu.Unlock()
126+
127+
todo, ok := s.todos[id]
128+
if !ok {
129+
return ErrTodoNotFound
130+
}
131+
132+
todo.Completed = completed
133+
134+
s.todos[id] = todo
135+
136+
return nil
137+
}

‎todo/service_test.go

+188
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
package todo
2+
3+
import (
4+
"context"
5+
"errors"
6+
"testing"
7+
8+
"github.com/google/go-cmp/cmp"
9+
)
10+
11+
func TestTodo(t *testing.T) {
12+
testcases := []struct {
13+
name string
14+
todos []Todo
15+
id int
16+
wantTodo Todo
17+
errMatch error
18+
}{
19+
{
20+
name: "successful-lookup",
21+
todos: []Todo{
22+
{
23+
Description: "do this task",
24+
},
25+
},
26+
id: 1,
27+
wantTodo: Todo{
28+
ID: 1,
29+
Description: "do this task",
30+
},
31+
},
32+
{
33+
name: "invalid-id",
34+
id: -1,
35+
errMatch: ErrInvalidID,
36+
},
37+
}
38+
39+
for _, tc := range testcases {
40+
t.Run(tc.name, func(t *testing.T) {
41+
svc := NewInmemService()
42+
ctx := context.Background()
43+
44+
for _, v := range tc.todos {
45+
if err := svc.CreateTodo(ctx, v.Description); err != nil {
46+
t.Fatalf("unexpected error creating initial set of Todo data %v", err)
47+
}
48+
}
49+
50+
gotTodo, gotErr := svc.Todo(ctx, tc.id)
51+
52+
if !errors.Is(gotErr, tc.errMatch) {
53+
t.Fatalf("unexpected error got %v, want %v", gotErr, tc.errMatch)
54+
}
55+
56+
if got, want := gotTodo, tc.wantTodo; got != want {
57+
t.Errorf("unexpected Todo returned got %+v, want %+v", got, want)
58+
}
59+
60+
})
61+
}
62+
}
63+
64+
func TestTodos(t *testing.T) {
65+
testcases := []struct {
66+
name string
67+
todos []Todo
68+
wantTodos []Todo
69+
}{
70+
{
71+
name: "no-items",
72+
},
73+
{
74+
name: "single-item",
75+
todos: []Todo{
76+
{
77+
Description: "desc 1",
78+
},
79+
},
80+
wantTodos: []Todo{
81+
{
82+
ID: 1,
83+
Description: "desc 1",
84+
},
85+
},
86+
},
87+
{
88+
name: "multiple-items",
89+
todos: []Todo{
90+
{
91+
Description: "desc 1",
92+
},
93+
{
94+
Description: "desc 2",
95+
},
96+
{
97+
Description: "desc 3",
98+
},
99+
},
100+
wantTodos: []Todo{
101+
{
102+
ID: 1,
103+
Description: "desc 1",
104+
},
105+
{
106+
ID: 2,
107+
Description: "desc 2",
108+
},
109+
{
110+
ID: 3,
111+
Description: "desc 3",
112+
},
113+
},
114+
},
115+
}
116+
117+
for _, tc := range testcases {
118+
t.Run(tc.name, func(t *testing.T) {
119+
svc := NewInmemService()
120+
ctx := context.Background()
121+
122+
for _, v := range tc.todos {
123+
if err := svc.CreateTodo(ctx, v.Description); err != nil {
124+
t.Fatalf("unexpected error inserting todo data %v", err)
125+
}
126+
}
127+
128+
gotTodos, err := svc.Todos(ctx)
129+
if err != nil {
130+
t.Fatalf("unexpected error fetching Todos %v", err)
131+
}
132+
133+
if got, want := gotTodos, tc.wantTodos; !cmp.Equal(got, want) {
134+
t.Errorf("unexpected todos returned (got: -, want: +) %v", cmp.Diff(got, want))
135+
}
136+
})
137+
}
138+
}
139+
140+
func TestUpdateTodo(t *testing.T) {
141+
testcases := []struct {
142+
name string
143+
todo Todo
144+
desc string
145+
wantTodo Todo
146+
errMatch error
147+
}{
148+
{
149+
name: "successful-update",
150+
todo: Todo{
151+
Description: "foo desc",
152+
},
153+
desc: "bar desc",
154+
wantTodo: Todo{
155+
ID: 1,
156+
Description: "bar desc",
157+
},
158+
},
159+
{
160+
name: "todo-not-found",
161+
errMatch: ErrTodoNotFound,
162+
},
163+
}
164+
165+
for _, tc := range testcases {
166+
t.Run(tc.name, func(t *testing.T) {
167+
svc := NewInmemService()
168+
ctx := context.Background()
169+
170+
if err := svc.CreateTodo(ctx, tc.todo.Description); err != nil {
171+
t.Fatalf("unexpected error creating initial Todo %v", err)
172+
}
173+
174+
if err := svc.UpdateTodo(ctx, tc.wantTodo.ID, tc.desc); !errors.Is(tc.errMatch, err) {
175+
t.Fatalf("unexpected error got %v, want %v", err, tc.errMatch)
176+
}
177+
178+
gotTodo, err := svc.Todo(ctx, tc.wantTodo.ID)
179+
if err != nil {
180+
t.Fatalf("unexpected error fetching updated Todo %v", err)
181+
}
182+
183+
if got, want := gotTodo, tc.wantTodo; !cmp.Equal(got, want) {
184+
t.Errorf("unexpected Todo returned (got: -, want: +) %v", cmp.Diff(got, want))
185+
}
186+
})
187+
}
188+
}

0 commit comments

Comments
 (0)
Please sign in to comment.