hikki 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +19 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +3 -0
  5. data/Gemfile +4 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +98 -0
  8. data/Rakefile +6 -0
  9. data/adapters/hikki-memcache/.rspec +2 -0
  10. data/adapters/hikki-memcache/.travis.yml +3 -0
  11. data/adapters/hikki-memcache/Gemfile +7 -0
  12. data/adapters/hikki-memcache/LICENSE.txt +22 -0
  13. data/adapters/hikki-memcache/README.md +50 -0
  14. data/adapters/hikki-memcache/Rakefile +6 -0
  15. data/adapters/hikki-memcache/hikki-memcache.gemspec +27 -0
  16. data/adapters/hikki-memcache/lib/hikki/adapters/memcache_adapter.rb +22 -0
  17. data/adapters/hikki-memcache/lib/hikki/adapters/memcache_collection.rb +54 -0
  18. data/adapters/hikki-memcache/spec/hikki/adapters/memcache_adapter_integration_spec.rb +39 -0
  19. data/adapters/hikki-memcache/spec/hikki/adapters/memcache_adapter_spec.rb +115 -0
  20. data/adapters/hikki-memcache/spec/spec_helper.rb +8 -0
  21. data/adapters/hikki-mongo/.rspec +2 -0
  22. data/adapters/hikki-mongo/.travis.yml +3 -0
  23. data/adapters/hikki-mongo/Gemfile +7 -0
  24. data/adapters/hikki-mongo/LICENSE.txt +22 -0
  25. data/adapters/hikki-mongo/README.md +44 -0
  26. data/adapters/hikki-mongo/Rakefile +6 -0
  27. data/adapters/hikki-mongo/hikki-mongo.gemspec +28 -0
  28. data/adapters/hikki-mongo/lib/hikki/adapters/mongo_adapter.rb +22 -0
  29. data/adapters/hikki-mongo/lib/hikki/adapters/mongo_collection.rb +86 -0
  30. data/adapters/hikki-mongo/spec/hikki/adapters/mongo_adapter_integration_spec.rb +39 -0
  31. data/adapters/hikki-mongo/spec/hikki/adapters/mongo_adapter_spec.rb +231 -0
  32. data/adapters/hikki-mongo/spec/spec_helper.rb +8 -0
  33. data/adapters/hikki-redis/.rspec +2 -0
  34. data/adapters/hikki-redis/.travis.yml +3 -0
  35. data/adapters/hikki-redis/Gemfile +7 -0
  36. data/adapters/hikki-redis/LICENSE.txt +22 -0
  37. data/adapters/hikki-redis/README.md +44 -0
  38. data/adapters/hikki-redis/Rakefile +6 -0
  39. data/adapters/hikki-redis/hikki-redis.gemspec +27 -0
  40. data/adapters/hikki-redis/lib/hikki/adapters/redis_adapter.rb +21 -0
  41. data/adapters/hikki-redis/lib/hikki/adapters/redis_collection.rb +105 -0
  42. data/adapters/hikki-redis/spec/hikki/adapters/redis_adapter_integration_spec.rb +39 -0
  43. data/adapters/hikki-redis/spec/hikki/adapters/redis_adapter_spec.rb +258 -0
  44. data/adapters/hikki-redis/spec/spec_helper.rb +8 -0
  45. data/all_specs +13 -0
  46. data/hikki.gemspec +24 -0
  47. data/lib/hikki.rb +9 -0
  48. data/lib/hikki/adapters/adapter.rb +43 -0
  49. data/lib/hikki/adapters/memory_adapter.rb +18 -0
  50. data/lib/hikki/adapters/memory_collection.rb +87 -0
  51. data/lib/hikki/collection.rb +62 -0
  52. data/lib/hikki/repository.rb +54 -0
  53. data/lib/hikki/version.rb +3 -0
  54. data/spec/hikki/adapters/memory_adapter_spec.rb +242 -0
  55. data/spec/hikki/repository_spec.rb +260 -0
  56. data/spec/hikki_spec.rb +5 -0
  57. data/spec/spec_helper.rb +2 -0
  58. metadata +146 -0
@@ -0,0 +1,8 @@
1
+ $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
2
+ require 'hikki/adapters/redis_adapter'
3
+ RSpec.configure do |config|
4
+ config.treat_symbols_as_metadata_keys_with_true_values = true
5
+ config.run_all_when_everything_filtered = true
6
+ config.filter_run :focus
7
+ config.filter_run_excluding :integration
8
+ end
data/all_specs ADDED
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env bash
2
+
3
+ rake
4
+ rspec spec --tag integration
5
+ cd adapters/hikki-redis
6
+ rake
7
+ rspec spec --tag integration
8
+ cd ../hikki-mongo
9
+ rake
10
+ rspec spec --tag integration
11
+ cd ../hikki-memcache
12
+ rake
13
+ rspec spec --tag integration
data/hikki.gemspec ADDED
@@ -0,0 +1,24 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'hikki/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'hikki'
8
+ spec.version = Hikki::VERSION
9
+ spec.authors = ['alexpeachey']
10
+ spec.email = ['alex.peachey@originate.com']
11
+ spec.summary = 'A light weight persistence system.'
12
+ spec.description = 'A light weight persistence system.'
13
+ spec.homepage = ''
14
+ spec.license = 'MIT'
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ['lib']
20
+
21
+ spec.add_development_dependency 'bundler', '~> 1.5'
22
+ spec.add_development_dependency 'rake'
23
+ spec.add_development_dependency 'rspec'
24
+ end
data/lib/hikki.rb ADDED
@@ -0,0 +1,9 @@
1
+ require 'hikki/version'
2
+ require 'hikki/collection'
3
+ require 'hikki/adapters/adapter'
4
+ require 'hikki/adapters/memory_collection'
5
+ require 'hikki/adapters/memory_adapter'
6
+ require 'hikki/repository'
7
+
8
+ module Hikki
9
+ end
@@ -0,0 +1,43 @@
1
+ module Hikki
2
+ module Adapters
3
+ class Adapter
4
+ attr_reader :collections
5
+
6
+ def initialize
7
+ @collections = {}
8
+ end
9
+
10
+ def index(collection, field)
11
+ collection_for(collection).index(field)
12
+ end
13
+
14
+ def save(collection, data)
15
+ collection_for(collection).save(data)
16
+ end
17
+
18
+ def find(collection, id)
19
+ collection_for(collection).find(id)
20
+ end
21
+
22
+ def all(collection, options={})
23
+ collection_for(collection).all(options)
24
+ end
25
+
26
+ def find_by(collection, field, value, options={})
27
+ collection_for(collection).find_by(field, value, options)
28
+ end
29
+
30
+ def remove(collection, id)
31
+ collection_for(collection).remove(id)
32
+ end
33
+
34
+ def remove_all(collection)
35
+ collection_for(collection).remove_all
36
+ end
37
+
38
+ def collection_for(collection)
39
+ collections.fetch(collection, Collection.new(collection, store, uuid_generator))
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,18 @@
1
+ module Hikki
2
+ module Adapters
3
+ class MemoryAdapter < Hikki::Adapters::Adapter
4
+ attr_reader :store, :uuid_generator, :collections
5
+
6
+ def initialize(uuid_generator=SecureRandom)
7
+ super()
8
+ @uuid_generator = uuid_generator
9
+ @store = {}
10
+ @collections = {}
11
+ end
12
+
13
+ def collection_for(collection)
14
+ collections.fetch(collection, MemoryCollection.new(collection, store, uuid_generator))
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,87 @@
1
+ module Hikki
2
+ module Adapters
3
+ class MemoryCollection < Hikki::Collection
4
+ attr_reader :store, :uuid_generator
5
+
6
+ def initialize(collection, store, uuid_generator)
7
+ super(collection)
8
+ @store = store
9
+ @uuid_generator = uuid_generator
10
+ end
11
+
12
+ def index(field)
13
+ store[index_key] ||= {}
14
+ store[index_key][field.to_s] ||= {}
15
+ true
16
+ end
17
+
18
+ def save(data)
19
+ data = normalize_data(data)
20
+ store[collection] ||= {}
21
+ store[collection][data['id']] = data
22
+ add_to_index(data)
23
+ data
24
+ end
25
+
26
+ def find(id)
27
+ store.fetch(collection, {}).fetch(id.to_s, {})
28
+ end
29
+
30
+ def all(options={})
31
+ options = normalize_options(options)
32
+ store.fetch(collection, {}).values[page_range(options)]
33
+ end
34
+
35
+ def find_by(field, value, options={})
36
+ options = normalize_options(options)
37
+ return find_by_index(field, value, options) if has_index?(field)
38
+ all.select { |o| o.fetch(field.to_s) == value }[page_range(options)]
39
+ end
40
+
41
+ def remove(id)
42
+ remove_from_index(find(id))
43
+ store.fetch(collection, {}).delete(id.to_s)
44
+ true
45
+ end
46
+
47
+ def remove_all
48
+ store.delete(collection)
49
+ store.delete(index_key)
50
+ true
51
+ end
52
+
53
+ def has_index?(field)
54
+ store.has_key?(index_key) &&
55
+ store[index_key].has_key?(field.to_s)
56
+ end
57
+
58
+ def id_for(data)
59
+ data.fetch('id', uuid_generator.uuid).to_s
60
+ end
61
+
62
+ private
63
+ def index_key
64
+ collection + '_index'
65
+ end
66
+
67
+ def find_by_index(field, value, options)
68
+ store[index_key][field.to_s].fetch(value, []).map { |id| find(id) }.reject { |v| v == {} }[page_range(options)]
69
+ end
70
+
71
+ def add_to_index(data)
72
+ store.fetch(index_key, {}).keys.each do |field|
73
+ store[index_key][field][data[field]] ||= []
74
+ store[index_key][field][data[field]] << data['id']
75
+ end
76
+ end
77
+
78
+ def remove_from_index(data)
79
+ return if data == {}
80
+ store.fetch(index_key, {}).keys.each do |field|
81
+ store[index_key][field].fetch(data[field], []).delete(data['id'])
82
+ end
83
+ end
84
+
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,62 @@
1
+ require 'json'
2
+
3
+ module Hikki
4
+ class Collection
5
+ attr_reader :collection
6
+
7
+ def initialize(collection)
8
+ @collection = collection.to_s
9
+ end
10
+
11
+ def index(field)
12
+ raise 'Implementation missing.'
13
+ end
14
+
15
+ def save(data)
16
+ raise 'Implementation missing.'
17
+ end
18
+
19
+ def find(id)
20
+ raise 'Implementation missing.'
21
+ end
22
+
23
+ def all(options)
24
+ raise 'Implementation missing.'
25
+ end
26
+
27
+ def find_by(field, value, options)
28
+ raise 'Implementation missing.'
29
+ end
30
+
31
+ def remove(id)
32
+ raise 'Implementation missing.'
33
+ end
34
+
35
+ def remove_all
36
+ raise 'Implementation missing.'
37
+ end
38
+
39
+ def normalize_data(data)
40
+ deep_copy(data).tap { |d| d.merge!('id' => id_for(d)) }
41
+ end
42
+
43
+ def deep_copy(h)
44
+ JSON.parse(h.to_json)
45
+ end
46
+
47
+ def id_for(data)
48
+ data.fetch('id', 'undefined').to_s
49
+ end
50
+
51
+ def normalize_options(options)
52
+ {
53
+ limit: options[:limit] || options['limit'] || -1,
54
+ offset: options[:offset] || options['offset'] || 0
55
+ }
56
+ end
57
+
58
+ def page_range(options)
59
+ (options[:offset]..(options[:limit] == -1 ? -1 : options[:offset] + options[:limit] - 1))
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,54 @@
1
+ module Hikki
2
+ class Repository
3
+
4
+ attr_reader :writers, :readers
5
+
6
+ def initialize(writers=[], readers=[])
7
+ @writers = Array(writers)
8
+ @writers << Hikki::Adapters::MemoryAdapter.new if @writers == []
9
+ @readers = Array(readers)
10
+ @readers << @writers.first if @readers == []
11
+ end
12
+
13
+ def save(collection, data)
14
+ writers.reduce({}) { |result, writer| writer.save(collection, data) }
15
+ end
16
+
17
+ def index(collection, field)
18
+ writers.all? { |writer| writer.index(collection, field) }
19
+ end
20
+
21
+ def remove(collection, field)
22
+ writers.all? { |writer| writer.remove(collection, field) }
23
+ end
24
+
25
+ def remove_all(collection)
26
+ writers.all? { |writer| writer.remove_all(collection) }
27
+ end
28
+
29
+ def find(collection, id)
30
+ readers.each do |reader|
31
+ result = reader.find(collection, id)
32
+ return result unless result == {}
33
+ end
34
+ {}
35
+ end
36
+
37
+ def all(collection, options={})
38
+ readers.each do |reader|
39
+ result = reader.all(collection, options)
40
+ return result unless result == []
41
+ end
42
+ []
43
+ end
44
+
45
+ def find_by(collection, field, value, options={})
46
+ readers.each do |reader|
47
+ result = reader.find_by(collection, field, value, options)
48
+ return result unless result == []
49
+ end
50
+ []
51
+ end
52
+
53
+ end
54
+ end
@@ -0,0 +1,3 @@
1
+ module Hikki
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,242 @@
1
+ require 'spec_helper'
2
+
3
+ module Hikki
4
+ module Adapters
5
+ describe MemoryAdapter do
6
+ subject(:adapter) { MemoryAdapter.new(uuid_generator) }
7
+ let(:uuid_generator) { double :generator, uuid: '12345' }
8
+ let(:collection) { 'collection1' }
9
+
10
+ describe '#index' do
11
+ it 'returns true' do
12
+ expect(adapter.index(collection, :field1)).to be_true
13
+ end
14
+
15
+ it 'creates the index in the store' do
16
+ adapter.index(collection, :field1)
17
+ expect(adapter.store['collection1_index']['field1']).to eq Hash.new
18
+ end
19
+ end
20
+
21
+ describe '#save' do
22
+ context 'when an id is provided in the data' do
23
+ let(:data) { { id: id, field1: 'test', field2: 123 } }
24
+ let(:expected) { { 'id' => id, 'field1' => 'test', 'field2' => 123 } }
25
+ let(:id) { '1' }
26
+
27
+ it 'returns the data' do
28
+ expect(adapter.save(collection, data)).to eq expected
29
+ end
30
+
31
+ it 'persists the data in the store' do
32
+ adapter.save(collection, data)
33
+ expect(adapter.store['collection1']['1']).to eq expected
34
+ end
35
+ end
36
+
37
+ context 'when an id is not provided in the data' do
38
+ let(:data) { { field1: 'test', field2: 123 } }
39
+ let(:expected) { { 'id' => id, 'field1' => 'test', 'field2' => 123 } }
40
+ let(:id) { '12345' }
41
+
42
+ it 'returns the data with the id added' do
43
+ expect(adapter.save(collection, data)).to eq expected
44
+ end
45
+
46
+ it 'persists the data in the store' do
47
+ adapter.save(collection, data)
48
+ expect(adapter.store['collection1']['12345']).to eq expected
49
+ end
50
+ end
51
+
52
+ context 'when an index exists on a field in the data' do
53
+ let(:data) { { id: id, field1: 'test', field2: 123 } }
54
+ let(:expected) { { 'id' => id, 'field1' => 'test', 'field2' => 123 } }
55
+ let(:id) { '1' }
56
+ before { adapter.index(collection, :field1) }
57
+
58
+ it 'adds an entry in the index' do
59
+ adapter.save(collection, data)
60
+ expect(adapter.store['collection1_index']['field1']['test']).to eq ['1']
61
+ end
62
+ end
63
+ end
64
+
65
+ describe '#find' do
66
+ let(:id) { '1' }
67
+
68
+ context 'when the id exists' do
69
+ let(:data) { { id: id, field1: 'test', field2: 123 } }
70
+ let(:expected) { { 'id' => id, 'field1' => 'test', 'field2' => 123 } }
71
+ before { adapter.save(collection, data) }
72
+
73
+ it 'retrieves the data' do
74
+ expect(adapter.find(collection, id)).to eq expected
75
+ end
76
+ end
77
+
78
+ context 'when the id does not exist' do
79
+ let(:expected) { {} }
80
+
81
+ it 'returns an empty hash' do
82
+ expect(adapter.find(collection, id)).to eq expected
83
+ end
84
+ end
85
+ end
86
+
87
+ describe '#all' do
88
+ context 'with a record in the collection' do
89
+ let(:data) { { id: '1', field1: 'test', field2: 123 } }
90
+ let(:expected) { { 'id' => '1', 'field1' => 'test', 'field2' => 123 } }
91
+ before { adapter.save(collection, data) }
92
+
93
+ it 'returns an array containing the record' do
94
+ expect(adapter.all(collection)).to eq [expected]
95
+ end
96
+ end
97
+
98
+ context 'with no records in the collection' do
99
+ it 'returns an array containing the record' do
100
+ expect(adapter.all(collection)).to eq []
101
+ end
102
+ end
103
+
104
+ context 'with multiple records and paging options specified' do
105
+ before do
106
+ 10.times do |i|
107
+ adapter.save(collection, {id: i+1, a: 1})
108
+ end
109
+ end
110
+
111
+ context 'with limit 2' do
112
+ let(:limit) { 2 }
113
+
114
+ it 'returns only 2 records' do
115
+ expect(adapter.all(collection, {limit: 2}).count).to eq 2
116
+ end
117
+
118
+ context 'with an offset of 4' do
119
+ it 'returns the 2 records starting with the offset' do
120
+ expect(adapter.all(collection, {limit: 2, offset: 4})).to eq [{'id' => '5', 'a' => 1}, {'id' => '6', 'a' => 1}]
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
126
+
127
+ describe '#find_by' do
128
+ context 'with a record in the collection matching' do
129
+ let(:data) { { id: '1', field1: 'test', field2: 123 } }
130
+ let(:expected) { { 'id' => '1', 'field1' => 'test', 'field2' => 123 } }
131
+
132
+ context 'without an index' do
133
+ before { adapter.save(collection, data) }
134
+
135
+ it 'returns an array containing the record' do
136
+ expect(adapter.find_by(collection, :field2, 123)).to eq [expected]
137
+ end
138
+ end
139
+
140
+ context 'with an index' do
141
+ before do
142
+ adapter.index(collection, :field1)
143
+ adapter.save(collection, data)
144
+ end
145
+
146
+ it 'returns an array containing the record' do
147
+ expect(adapter.find_by(collection, :field1, 'test')).to eq [expected]
148
+ end
149
+ end
150
+ end
151
+
152
+ context 'with no matching records in the collection' do
153
+ it 'returns an array containing the record' do
154
+ expect(adapter.find_by(collection, :field1, 'foo')).to eq []
155
+ end
156
+ end
157
+
158
+ context 'with multiple matching records and paging options specified' do
159
+ before do
160
+ 10.times do |i|
161
+ adapter.save(collection, {id: i+1, a: 1})
162
+ end
163
+ end
164
+
165
+ context 'with limit 2' do
166
+ let(:limit) { 2 }
167
+
168
+ it 'returns only 2 records' do
169
+ expect(adapter.find_by(collection, :a, 1, {limit: 2}).count).to eq 2
170
+ end
171
+
172
+ context 'with an offset of 4' do
173
+ it 'returns the 2 records starting with the offset' do
174
+ expect(adapter.find_by(collection, :a, 1, {limit: 2, offset: 4})).to eq [{'id' => '5', 'a' => 1}, {'id' => '6', 'a' => 1}]
175
+ end
176
+ end
177
+ end
178
+ end
179
+ end
180
+
181
+ describe '#remove' do
182
+ context 'with a record for the id in the collection' do
183
+ let(:data) { { id: id, field1: 'test', field2: 123 } }
184
+ let(:id) { '1' }
185
+ before { adapter.save(collection, data) }
186
+
187
+ it 'returns true' do
188
+ expect(adapter.remove(collection, id)).to be_true
189
+ end
190
+
191
+ it 'removes the record from the store' do
192
+ adapter.remove(collection, id)
193
+ expect(adapter.store[collection][id]).to be_nil
194
+ end
195
+
196
+ context 'when there is an index' do
197
+ before do
198
+ adapter.index(collection, :field1)
199
+ adapter.save(collection, data)
200
+ end
201
+
202
+ it 'removes the id from the index' do
203
+ expect(adapter.store[collection + '_index']['field1']['test']).to eq ['1']
204
+ adapter.remove(collection, id)
205
+ expect(adapter.store[collection + '_index']['field1']['test']).to eq []
206
+ end
207
+ end
208
+ end
209
+ end
210
+
211
+ describe '#remove_all' do
212
+ context 'with a record in the collection' do
213
+ let(:data) { { id: id, field1: 'test', field2: 123 } }
214
+ let(:id) { '1' }
215
+ before { adapter.save(collection, data) }
216
+
217
+ it 'returns true' do
218
+ expect(adapter.remove_all(collection)).to be_true
219
+ end
220
+
221
+ it 'removes all records from the store' do
222
+ adapter.remove_all(collection)
223
+ expect(adapter.store[collection]).to be_nil
224
+ end
225
+
226
+ context 'when there is an index' do
227
+ before do
228
+ adapter.index(collection, :field1)
229
+ adapter.save(collection, data)
230
+ end
231
+
232
+ it 'removes the id from the index' do
233
+ expect(adapter.store[collection + '_index']['field1']['test']).to eq ['1']
234
+ adapter.remove_all(collection)
235
+ expect(adapter.store[collection + '_index']).to be_nil
236
+ end
237
+ end
238
+ end
239
+ end
240
+ end
241
+ end
242
+ end