Skip to content

Commit 0b7a046

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 0f11d33 commit 0b7a046

File tree

9 files changed

+356
-28
lines changed

9 files changed

+356
-28
lines changed

internal/database/consul/database.go

Lines changed: 24 additions & 2 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

@@ -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 interface{}) 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: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package consul
2+
3+
import (
4+
"context"
5+
"testing"
6+
"tracker/internal/types/show"
7+
8+
"github.com/go-test/deep"
9+
)
10+
11+
var testData = map[string][]byte{
12+
"tracker/shows/list/westworld": []byte(`{"id": 1, "name": "Westworld"}`),
13+
"tracker/shows/episodes/westworld_s01e01": []byte(`{"title":"The Original", "season": 1, "episode": 1}`),
14+
15+
"tracker/shows/list/expanse": []byte(`{"id": 2, "name": "The Expanse"}`),
16+
}
17+
18+
func TestList(t *testing.T) {
19+
kv := &testKV{m: testData}
20+
db, err := NewDatabase("tracker", KVClient(kv))
21+
if err != nil {
22+
t.Fatalf("unable to setup database: %v", err)
23+
}
24+
25+
showsDB := db.Shows()
26+
27+
want := []*show.Show{
28+
{ID: 2, Name: "The Expanse"},
29+
{ID: 1, Name: "Westworld"},
30+
}
31+
32+
got, err := showsDB.List(context.Background())
33+
if err != nil {
34+
t.Fatalf("unexpected error listing shows: %v", err)
35+
}
36+
37+
if diff := deep.Equal(got, want); diff != nil {
38+
t.Fatalf("List() = %v, want %v, diff = %v", got, want, diff)
39+
}
40+
}
41+
42+
func TestDetails(t *testing.T) {
43+
kv := &testKV{m: testData}
44+
db, err := NewDatabase("tracker", KVClient(kv))
45+
if err != nil {
46+
t.Fatalf("unable to setup database: %v", err)
47+
}
48+
49+
showsDB := db.Shows()
50+
51+
want := &show.Show{
52+
ID: 1,
53+
Name: "Westworld",
54+
Episodes: []*show.Episode{
55+
{Title: "The Original", Season: 1, Episode: 1},
56+
},
57+
}
58+
59+
got, err := showsDB.Details(context.Background(), "westworld")
60+
if err != nil {
61+
t.Fatalf("unexpected error getting details: %v", err)
62+
}
63+
64+
if diff := deep.Equal(got, want); diff != nil {
65+
t.Fatalf("Details() = %v, want %v, diff = %v", got, want, diff)
66+
}
67+
68+
}
Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,8 @@ 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) {
1815
var i interface{} = &UsersDatabase{}
1916

@@ -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)