hikki 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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