Skip to content

Commit 0bfae00

Browse files
Wojciech BednarzakVoyTechnology
authored andcommitted
Add database interface for shows and episode
This abstracts getting the data from the database. In the commit there are 2 implementations: consul and sql.
1 parent 7dc07ee commit 0bfae00

File tree

13 files changed

+414
-32
lines changed

13 files changed

+414
-32
lines changed

internal/database/consul/database.go

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@ import (
66
"encoding/json"
77
"fmt"
88
"path"
9+
"strings"
910

1011
"github.com/hashicorp/consul/api"
1112

1213
"tracker/internal/database"
1314
)
1415

15-
// UserDatabase implements database.UserDatabase and can be used
16-
// to insert user data.
16+
// Database contains methods for getting the data about shows and episodes
1717
type Database struct {
1818
kv KV
1919

@@ -39,7 +39,7 @@ func NewDatabase(prefix string, opts ...Option) (*Database, error) {
3939
}
4040

4141
// get value from the database
42-
func (db *Database) get(ctx context.Context, key string, value interface{}) error {
42+
func (db *Database) get(ctx context.Context, key string, value any) error {
4343
opt := &api.QueryOptions{}
4444
opt = opt.WithContext(ctx)
4545

@@ -60,7 +60,7 @@ func (db *Database) get(ctx context.Context, key string, value interface{}) erro
6060
return nil
6161
}
6262

63-
func (db *Database) put(ctx context.Context, key string, value interface{}) error {
63+
func (db *Database) put(ctx context.Context, key string, value any) error {
6464
opt := &api.WriteOptions{}
6565
opt = opt.WithContext(ctx)
6666

@@ -79,6 +79,27 @@ func (db *Database) put(ctx context.Context, key string, value interface{}) erro
7979
}
8080

8181
// Option allows to set options for the Consul database.type Option func(*Database)
82+
func (db *Database) list(ctx context.Context, prefix string) (map[string][]byte, error) {
83+
opt := &api.QueryOptions{}
84+
opt = opt.WithContext(ctx)
85+
86+
p := path.Join(db.prefix, prefix)
87+
88+
kvs, _, err := db.kv.List(p, opt)
89+
if err != nil {
90+
return nil, fmt.Errorf("unable to list %s: %w", p, err)
91+
}
92+
93+
m := make(map[string][]byte, len(kvs))
94+
for _, kv := range kvs {
95+
// Remove all the prefixes from the keys.
96+
m[strings.TrimPrefix(kv.Key, p+"/")] = kv.Value
97+
}
98+
99+
return m, nil
100+
}
101+
102+
// Option allows to set options for the Consul database.
82103
type Option func(*Database)
83104

84105
func KVClient(kv KV) Option {
@@ -91,4 +112,5 @@ func KVClient(kv KV) Option {
91112
type KV interface {
92113
Put(p *api.KVPair, q *api.WriteOptions) (*api.WriteMeta, error)
93114
Get(key string, q *api.QueryOptions) (*api.KVPair, *api.QueryMeta, error)
115+
List(prefix string, q *api.QueryOptions) (api.KVPairs, *api.QueryMeta, error)
94116
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package consul
2+
3+
import (
4+
"errors"
5+
"strings"
6+
7+
"github.com/hashicorp/consul/api"
8+
)
9+
10+
var errNotFound = errors.New("kv: not found")
11+
12+
type testKV struct {
13+
m map[string][]byte
14+
}
15+
16+
func (kv *testKV) Get(key string, _ *api.QueryOptions) (*api.KVPair, *api.QueryMeta, error) {
17+
if v, exists := kv.m[key]; exists {
18+
return &api.KVPair{Key: key, Value: v}, nil, nil
19+
} else {
20+
return nil, nil, errNotFound
21+
}
22+
}
23+
24+
func (kv *testKV) Put(pair *api.KVPair, _ *api.WriteOptions) (*api.WriteMeta, error) {
25+
kv.m[pair.Key] = pair.Value
26+
return nil, nil
27+
}
28+
29+
func (kv *testKV) List(prefix string, _ *api.QueryOptions) (api.KVPairs, *api.QueryMeta, error) {
30+
var pairs []*api.KVPair
31+
for key, value := range kv.m {
32+
if strings.HasPrefix(key, prefix) {
33+
pairs = append(pairs, &api.KVPair{Key: key, Value: value})
34+
}
35+
}
36+
37+
return api.KVPairs(pairs), nil, nil
38+
}

internal/database/consul/shows.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package consul
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"path"
8+
9+
"tracker/internal/types/show"
10+
)
11+
12+
type ShowsDatabase struct {
13+
db *Database
14+
prefix string
15+
}
16+
17+
func (db *Database) Shows() *ShowsDatabase {
18+
return &ShowsDatabase{
19+
db: db,
20+
prefix: "shows",
21+
}
22+
}
23+
24+
func (db *ShowsDatabase) List(ctx context.Context) ([]*show.Show, error) {
25+
showIDs, err := db.list(ctx, "list")
26+
if err != nil {
27+
return nil, fmt.Errorf("unable to get show IDs")
28+
}
29+
30+
// TODO: Speed up by running these in parallel
31+
var shows []*show.Show
32+
for showID := range showIDs {
33+
show, err := db.showDetails(ctx, showID)
34+
if err != nil {
35+
return nil, fmt.Errorf("unable to get show %s: %w", showID, err)
36+
}
37+
shows = append(shows, show)
38+
}
39+
40+
return shows, nil
41+
}
42+
43+
func (db *ShowsDatabase) Details(ctx context.Context, id string) (*show.Show, error) {
44+
// TODO: Run getting show details and show episodes in parallel
45+
s, err := db.showDetails(ctx, id)
46+
if err != nil {
47+
return nil, fmt.Errorf("unable to get details about %s: %w", id, err)
48+
}
49+
50+
episodes, err := db.list(ctx, path.Join("episodes", id))
51+
if err != nil {
52+
return nil, fmt.Errorf("unable to get episodes for %s: %w", id, err)
53+
}
54+
55+
for _, ep := range episodes {
56+
var episode show.Episode
57+
if err := json.Unmarshal(ep, &episode); err != nil {
58+
return nil, fmt.Errorf("unable to parse episode: %w", err)
59+
}
60+
s.Episodes = append(s.Episodes, &episode)
61+
}
62+
63+
return s, nil
64+
}
65+
66+
func (db *ShowsDatabase) showDetails(ctx context.Context, id string) (*show.Show, error) {
67+
var show show.Show
68+
if err := db.get(ctx, path.Join("list", id), &show); err != nil {
69+
return nil, err
70+
}
71+
72+
return &show, nil
73+
}
74+
75+
func (db *ShowsDatabase) get(ctx context.Context, key string, value any) error {
76+
return db.db.get(ctx, path.Join(db.prefix, key), value)
77+
}
78+
79+
func (db *ShowsDatabase) list(ctx context.Context, prefix string) (map[string][]byte, error) {
80+
return db.db.list(ctx, path.Join(db.prefix, prefix))
81+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package consul
2+
3+
import (
4+
"context"
5+
"sort"
6+
"testing"
7+
"tracker/internal/types/show"
8+
9+
"github.com/go-test/deep"
10+
)
11+
12+
var testData = map[string][]byte{
13+
"tracker/shows/list/westworld": []byte(`{"id": 1, "name": "Westworld"}`),
14+
"tracker/shows/episodes/westworld_s01e01": []byte(`{"title":"The Original", "season": 1, "episode": 1}`),
15+
16+
"tracker/shows/list/expanse": []byte(`{"id": 2, "name": "The Expanse"}`),
17+
}
18+
19+
func TestList(t *testing.T) {
20+
kv := &testKV{m: testData}
21+
db, err := NewDatabase("tracker", KVClient(kv))
22+
if err != nil {
23+
t.Fatalf("unable to setup database: %v", err)
24+
}
25+
26+
showsDB := db.Shows()
27+
28+
want := []*show.Show{
29+
{ID: 2, Name: "The Expanse"},
30+
{ID: 1, Name: "Westworld"},
31+
}
32+
33+
got, err := showsDB.List(context.Background())
34+
if err != nil {
35+
t.Fatalf("unexpected error listing shows: %v", err)
36+
}
37+
38+
sort.Sort(show.ByName(got))
39+
40+
if diff := deep.Equal(got, want); diff != nil {
41+
t.Fatalf("List() = %v, want %v, diff = %v", got, want, diff)
42+
}
43+
}
44+
45+
func TestDetails(t *testing.T) {
46+
kv := &testKV{m: testData}
47+
db, err := NewDatabase("tracker", KVClient(kv))
48+
if err != nil {
49+
t.Fatalf("unable to setup database: %v", err)
50+
}
51+
52+
showsDB := db.Shows()
53+
54+
want := &show.Show{
55+
ID: 1,
56+
Name: "Westworld",
57+
Episodes: []*show.Episode{
58+
{Title: "The Original", Season: 1, Episode: 1},
59+
},
60+
}
61+
62+
got, err := showsDB.Details(context.Background(), "westworld")
63+
if err != nil {
64+
t.Fatalf("unexpected error getting details: %v", err)
65+
}
66+
67+
if diff := deep.Equal(got, want); diff != nil {
68+
t.Fatalf("Details() = %v, want %v, diff = %v", got, want, diff)
69+
}
70+
71+
}

internal/database/consul/users.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,10 @@ func (db *UsersDatabase) Details(ctx context.Context, email string) (*user.User,
3939
return &u, nil
4040
}
4141

42-
func (db *UsersDatabase) get(ctx context.Context, key string, value interface{}) error {
42+
func (db *UsersDatabase) get(ctx context.Context, key string, value any) error {
4343
return db.db.get(ctx, path.Join(db.prefix, key), value)
4444
}
4545

46-
func (db *UsersDatabase) put(ctx context.Context, key string, value interface{}) error {
46+
func (db *UsersDatabase) put(ctx context.Context, key string, value any) error {
4747
return db.db.put(ctx, path.Join(db.prefix, key), value)
4848
}
Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,10 @@ import (
99
"tracker/internal/types/user"
1010

1111
"github.com/go-test/deep"
12-
"github.com/hashicorp/consul/api"
1312
)
1413

15-
var errNotFound = errors.New("kv: not found")
16-
1714
func TestImplements(t *testing.T) {
18-
var i interface{} = &UsersDatabase{}
15+
var i any = &UsersDatabase{}
1916

2017
if _, ok := i.(database.UsersDatabase); !ok {
2118
t.Errorf("UserDatabase does not implement database.UserDatabase")
@@ -56,20 +53,3 @@ func TestE2E(t *testing.T) {
5653
t.Fatalf("Get() = %v, got %v, diff = %v", got, want, diff)
5754
}
5855
}
59-
60-
type testKV struct {
61-
m map[string][]byte
62-
}
63-
64-
func (kv *testKV) Put(pair *api.KVPair, _ *api.WriteOptions) (*api.WriteMeta, error) {
65-
kv.m[pair.Key] = pair.Value
66-
return nil, nil
67-
}
68-
69-
func (kv *testKV) Get(key string, _ *api.QueryOptions) (*api.KVPair, *api.QueryMeta, error) {
70-
if v, exists := kv.m[key]; exists {
71-
return &api.KVPair{Key: key, Value: v}, nil, nil
72-
} else {
73-
return nil, nil, errNotFound
74-
}
75-
}

internal/database/database.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package database
33
import (
44
"context"
55

6+
"tracker/internal/types/show"
67
"tracker/internal/types/user"
78
)
89

@@ -16,9 +17,10 @@ const (
1617
ErrNotFound = Error("not found")
1718
)
1819

19-
// Database
20+
// Database is the shared abstraction over all databases
2021
type Database interface {
2122
Users() UsersDatabase
23+
Shows() ShowsDatabase
2224
}
2325

2426
// UserDatabase abstracts the user interaction with the database.
@@ -28,3 +30,10 @@ type UsersDatabase interface {
2830
// Details the user based on the email address.
2931
Details(ctx context.Context, email string) (*user.User, error)
3032
}
33+
34+
type ShowsDatabase interface {
35+
// List returns the list of shows. The episode list is empty in each show.
36+
List(context.Context) ([]*show.Show, error)
37+
// Details gives details about a show by a show ID, including the episodes.
38+
Details(context.Context, int) (*show.Show, error)
39+
}

0 commit comments

Comments
 (0)