prologic revised this gist . Go to revision
2 files changed, 15 insertions, 10 deletions
collection.go
@@ -4,6 +4,7 @@ import ( | |||
4 | 4 | "encoding/json" | |
5 | 5 | "errors" | |
6 | 6 | "fmt" | |
7 | + | "reflect" | |
7 | 8 | ) | |
8 | 9 | ||
9 | 10 | var ( | |
@@ -62,22 +63,26 @@ func (c *Collection) Get(id string, obj any) error { | |||
62 | 63 | } | |
63 | 64 | ||
64 | 65 | // List returns a list of all objects in this collection. | |
65 | - | func (c *Collection) List(objects []any) error { | |
66 | + | func (c *Collection) List(objects any) error { | |
66 | 67 | tx := c.db.Transaction() | |
67 | 68 | defer tx.Discard() | |
68 | 69 | ||
70 | + | value := reflect.ValueOf(objects).Elem() | |
71 | + | elem := reflect.TypeOf(objects).Elem() | |
72 | + | ||
69 | 73 | return tx.Scan(c.makePrefix(), func(k Key) error { | |
70 | 74 | data, err := tx.Get(k) | |
71 | 75 | if err != nil { | |
72 | 76 | return err | |
73 | 77 | } | |
74 | 78 | ||
75 | - | var obj any | |
76 | - | if err := json.Unmarshal(data, obj); err != nil { | |
79 | + | obj := reflect.New(elem) | |
80 | + | ||
81 | + | if err := json.Unmarshal(data, &obj); err != nil { | |
77 | 82 | return err | |
78 | 83 | } | |
79 | 84 | ||
80 | - | objects = append(objects, obj) | |
85 | + | value.Set(reflect.Append(value, reflect.ValueOf(obj))) | |
81 | 86 | ||
82 | 87 | return nil | |
83 | 88 | }) | |
@@ -117,4 +122,4 @@ func (c *Collection) Drop() error { | |||
117 | 122 | // Count() > 0. | |
118 | 123 | func (c *Collection) Exists() bool { | |
119 | 124 | return c.Count() > 0 | |
120 | - | } | |
125 | + | } |
collection_test.go
@@ -99,7 +99,7 @@ func TestCollection(t *testing.T) { | |||
99 | 99 | c := db.Collection("users") | |
100 | 100 | ||
101 | 101 | var actual []User | |
102 | - | expected := []User{} | |
102 | + | var expected []User | |
103 | 103 | // cannot use actual (variable of type []User) as []any value in argument to c.List | |
104 | 104 | assert.NoError(t, c.List(actual)) | |
105 | 105 | assert.Equal(t, expected, actual) | |
@@ -110,13 +110,13 @@ func TestCollection(t *testing.T) { | |||
110 | 110 | ||
111 | 111 | actual = []User{} | |
112 | 112 | expected = []User{ | |
113 | - | User{"James", 21}, | |
114 | - | User{"Bob", 99}, | |
115 | - | User{"frank", 37}, | |
113 | + | {"James", 21}, | |
114 | + | {"Bob", 99}, | |
115 | + | {"frank", 37}, | |
116 | 116 | } | |
117 | 117 | ||
118 | 118 | // cannot use actual (variable of type []User) as []any value in argument to c.List | |
119 | 119 | assert.NoError(t, c.List(actual)) | |
120 | 120 | assert.Equal(t, expected, actual) | |
121 | 121 | }) | |
122 | - | } | |
122 | + | } |
prologic revised this gist . Go to revision
2 files changed, 242 insertions
collection.go(file created)
@@ -0,0 +1,120 @@ | |||
1 | + | package bitcask | |
2 | + | ||
3 | + | import ( | |
4 | + | "encoding/json" | |
5 | + | "errors" | |
6 | + | "fmt" | |
7 | + | ) | |
8 | + | ||
9 | + | var ( | |
10 | + | // ErrObjectNotFound is the error returned when an object is not found in the collection. | |
11 | + | ErrObjectNotFound = errors.New("error: object not found") | |
12 | + | ) | |
13 | + | ||
14 | + | func (b *bitcask) Collection(name string) *Collection { | |
15 | + | return &Collection{db: b, name: name} | |
16 | + | } | |
17 | + | ||
18 | + | // Collection allows you to manage a collection of objects encoded as JSON | |
19 | + | // documents with a path-based key based on the provided name. This is convenient | |
20 | + | // for storing complex normalized collections of objects. | |
21 | + | type Collection struct { | |
22 | + | db DB | |
23 | + | name string | |
24 | + | } | |
25 | + | ||
26 | + | func (c *Collection) makeKey(id string) Key { | |
27 | + | return Key(fmt.Sprintf("%s/%s", c.name, id)) | |
28 | + | } | |
29 | + | ||
30 | + | func (c *Collection) makePrefix() Key { | |
31 | + | return Key(c.name) | |
32 | + | } | |
33 | + | ||
34 | + | // Add adds a new object to the collection | |
35 | + | func (c *Collection) Add(id string, obj any) error { | |
36 | + | k := c.makeKey(id) | |
37 | + | v, err := json.Marshal(obj) | |
38 | + | if err != nil { | |
39 | + | return err | |
40 | + | } | |
41 | + | ||
42 | + | return c.db.Put(k, v) | |
43 | + | } | |
44 | + | ||
45 | + | // Delete deletes an object from the collection | |
46 | + | func (c *Collection) Delete(id string) error { | |
47 | + | return c.db.Delete(c.makeKey(id)) | |
48 | + | } | |
49 | + | ||
50 | + | // Get returns an object from the collection | |
51 | + | func (c *Collection) Get(id string, obj any) error { | |
52 | + | k := c.makeKey(id) | |
53 | + | data, err := c.db.Get(k) | |
54 | + | if err != nil { | |
55 | + | if err == ErrKeyNotFound { | |
56 | + | return ErrObjectNotFound | |
57 | + | } | |
58 | + | return err | |
59 | + | } | |
60 | + | ||
61 | + | return json.Unmarshal(data, obj) | |
62 | + | } | |
63 | + | ||
64 | + | // List returns a list of all objects in this collection. | |
65 | + | func (c *Collection) List(objects []any) error { | |
66 | + | tx := c.db.Transaction() | |
67 | + | defer tx.Discard() | |
68 | + | ||
69 | + | return tx.Scan(c.makePrefix(), func(k Key) error { | |
70 | + | data, err := tx.Get(k) | |
71 | + | if err != nil { | |
72 | + | return err | |
73 | + | } | |
74 | + | ||
75 | + | var obj any | |
76 | + | if err := json.Unmarshal(data, obj); err != nil { | |
77 | + | return err | |
78 | + | } | |
79 | + | ||
80 | + | objects = append(objects, obj) | |
81 | + | ||
82 | + | return nil | |
83 | + | }) | |
84 | + | } | |
85 | + | ||
86 | + | // Has returns true if an object exists by the provided id. | |
87 | + | func (c *Collection) Has(id string) bool { | |
88 | + | return c.db.Has(c.makeKey(id)) | |
89 | + | } | |
90 | + | ||
91 | + | // Count returns the number of objects in this collection. | |
92 | + | func (c *Collection) Count() int { | |
93 | + | n := 0 | |
94 | + | c.db.Scan(c.makePrefix(), func(k Key) error { | |
95 | + | n++ | |
96 | + | return nil | |
97 | + | }) | |
98 | + | return n | |
99 | + | } | |
100 | + | ||
101 | + | // Drop deletes the entire collection | |
102 | + | func (c *Collection) Drop() error { | |
103 | + | tx := c.db.Transaction() | |
104 | + | defer tx.Discard() | |
105 | + | ||
106 | + | if err := tx.Scan(c.makePrefix(), func(k Key) error { | |
107 | + | return tx.Delete(k) | |
108 | + | }); err != nil { | |
109 | + | return err | |
110 | + | } | |
111 | + | ||
112 | + | return tx.Commit() | |
113 | + | } | |
114 | + | ||
115 | + | // Exists returns true if the collection exists at all, which really just means | |
116 | + | // whether there are any objects in the collection, so this is the same as calling | |
117 | + | // Count() > 0. | |
118 | + | func (c *Collection) Exists() bool { | |
119 | + | return c.Count() > 0 | |
120 | + | } |
collection_test.go(file created)
@@ -0,0 +1,122 @@ | |||
1 | + | package bitcask | |
2 | + | ||
3 | + | import ( | |
4 | + | "os" | |
5 | + | "testing" | |
6 | + | ||
7 | + | "github.com/stretchr/testify/assert" | |
8 | + | "github.com/stretchr/testify/require" | |
9 | + | ) | |
10 | + | ||
11 | + | func setupDB(t *testing.T) DB { | |
12 | + | t.Helper() | |
13 | + | ||
14 | + | testDir, err := os.MkdirTemp("", "bitcask") | |
15 | + | assert.NoError(t, err) | |
16 | + | ||
17 | + | db, err := Open(testDir) | |
18 | + | assert.NoError(t, err) | |
19 | + | ||
20 | + | return db | |
21 | + | } | |
22 | + | ||
23 | + | func TestCollection(t *testing.T) { | |
24 | + | type User struct { | |
25 | + | Name string | |
26 | + | Age int | |
27 | + | } | |
28 | + | ||
29 | + | t.Run("AddGetDelete", func(t *testing.T) { | |
30 | + | db := setupDB(t) | |
31 | + | defer db.Close() | |
32 | + | c := db.Collection("users") | |
33 | + | ||
34 | + | err := c.Add("prologic", &User{"James", 21}) | |
35 | + | assert.NoError(t, err) | |
36 | + | ||
37 | + | var actual User | |
38 | + | expected := User{"James", 21} | |
39 | + | require.NoError(t, c.Get("prologic", &actual)) | |
40 | + | assert.EqualValues(t, expected, actual) | |
41 | + | ||
42 | + | actual = User{} | |
43 | + | expected = User{} | |
44 | + | require.NoError(t, c.Delete("prologic")) | |
45 | + | err = c.Get("prologic", &actual) | |
46 | + | require.Error(t, err) | |
47 | + | assert.EqualValues(t, expected, actual) | |
48 | + | }) | |
49 | + | ||
50 | + | t.Run("GetError", func(t *testing.T) { | |
51 | + | db := setupDB(t) | |
52 | + | defer db.Close() | |
53 | + | c := db.Collection("users") | |
54 | + | ||
55 | + | var actual User | |
56 | + | err := c.Get("foo", &actual) | |
57 | + | require.Error(t, err) | |
58 | + | assert.ErrorIs(t, err, ErrObjectNotFound) | |
59 | + | assert.EqualError(t, err, ErrObjectNotFound.Error()) | |
60 | + | }) | |
61 | + | ||
62 | + | t.Run("CountExistsEmpty", func(t *testing.T) { | |
63 | + | db := setupDB(t) | |
64 | + | defer db.Close() | |
65 | + | c := db.Collection("users") | |
66 | + | ||
67 | + | assert.Zero(t, c.Count()) | |
68 | + | assert.False(t, c.Exists()) | |
69 | + | }) | |
70 | + | ||
71 | + | t.Run("CountNonZero", func(t *testing.T) { | |
72 | + | db := setupDB(t) | |
73 | + | defer db.Close() | |
74 | + | c := db.Collection("users") | |
75 | + | ||
76 | + | assert.Zero(t, c.Count()) | |
77 | + | assert.False(t, c.Exists()) | |
78 | + | ||
79 | + | require.NoError(t, c.Add("prologic", User{"James", 21})) | |
80 | + | assert.Equal(t, 1, c.Count()) | |
81 | + | assert.True(t, c.Exists()) | |
82 | + | ||
83 | + | }) | |
84 | + | ||
85 | + | t.Run("Has", func(t *testing.T) { | |
86 | + | db := setupDB(t) | |
87 | + | defer db.Close() | |
88 | + | c := db.Collection("users") | |
89 | + | ||
90 | + | assert.False(t, c.Has("prologic")) | |
91 | + | ||
92 | + | require.NoError(t, c.Add("prologic", User{"James", 21})) | |
93 | + | assert.True(t, c.Has("prologic")) | |
94 | + | }) | |
95 | + | ||
96 | + | t.Run("List", func(t *testing.T) { | |
97 | + | db := setupDB(t) | |
98 | + | defer db.Close() | |
99 | + | c := db.Collection("users") | |
100 | + | ||
101 | + | var actual []User | |
102 | + | expected := []User{} | |
103 | + | // cannot use actual (variable of type []User) as []any value in argument to c.List | |
104 | + | assert.NoError(t, c.List(actual)) | |
105 | + | assert.Equal(t, expected, actual) | |
106 | + | ||
107 | + | require.NoError(t, c.Add("prologic", User{"James", 21})) | |
108 | + | require.NoError(t, c.Add("bob", User{"Bob", 99})) // name is made-up | |
109 | + | require.NoError(t, c.Add("frank", User{"Frank", 37})) // name is made-up | |
110 | + | ||
111 | + | actual = []User{} | |
112 | + | expected = []User{ | |
113 | + | User{"James", 21}, | |
114 | + | User{"Bob", 99}, | |
115 | + | User{"frank", 37}, | |
116 | + | } | |
117 | + | ||
118 | + | // cannot use actual (variable of type []User) as []any value in argument to c.List | |
119 | + | assert.NoError(t, c.List(actual)) | |
120 | + | assert.Equal(t, expected, actual) | |
121 | + | }) | |
122 | + | } |