memstore 1.1.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -29,10 +29,10 @@ $ gem install memstore
29
29
  ## Usage
30
30
 
31
31
  - [Basics](#basics)
32
- - [Objects vs. Hashes](#objects-vs-hashes)
33
- - [Adding Items](#adding-items)
34
- - [Retrieving Items](#retrieving-items)
35
- - [Deleting Items](#deleting-items)
32
+ - [Objects vs. Hashes](#objects-vs-hashes)
33
+ - [Adding Items](#adding-items)
34
+ - [Retrieving Items](#retrieving-items)
35
+ - [Deleting Items](#deleting-items)
36
36
  - [Search Queries](#search-queries)
37
37
  - [Serialization](#serialization)
38
38
  - [Binary](#binary)
@@ -40,6 +40,7 @@ $ gem install memstore
40
40
  - [YAML](#yaml)
41
41
  - [JSON](#json)
42
42
  - [MessagePack](#messagepack)
43
+ - [Concurrent Access](#concurrent-access)
43
44
 
44
45
  ### Basics
45
46
 
@@ -68,7 +69,7 @@ store = MemStore.new(:id, { ... }) # to use custom key
68
69
 
69
70
  The collection must be a hash that correctly maps the used key to each item.
70
71
 
71
- ### Objects vs. Hashes
72
+ #### Objects vs. Hashes
72
73
 
73
74
  MemStore comes in two flavors: `ObjectStore` and `HashStore`.
74
75
 
@@ -90,7 +91,7 @@ store = MemStore::HashStore.new
90
91
 
91
92
  If no key attribute is specified, `HashStore` will also use `Object#hash`.
92
93
 
93
- ### Adding Items
94
+ #### Adding Items
94
95
 
95
96
  `items` provides direct access to the internal items hash.
96
97
 
@@ -123,7 +124,7 @@ store << a << b << c
123
124
  # => store
124
125
  ```
125
126
 
126
- ### Retrieving Items
127
+ #### Retrieving Items
127
128
 
128
129
  `length` (or `size`) returns the current number of items:
129
130
 
@@ -152,7 +153,7 @@ store[1..3, 6]
152
153
  # => [a, b, c, f]
153
154
  ```
154
155
 
155
- ### Deleting Items
156
+ #### Deleting Items
156
157
 
157
158
  `delete_items` (or `delete_item`) deletes items by reference and returns them.
158
159
  This is considered the default use case and therefore also available as `delete`.
@@ -187,17 +188,17 @@ store.delete_keys(5..7, 9)
187
188
 
188
189
  The following methods are available to query the data store:
189
190
 
190
- - `find_all` (also available as `find`)
191
+ - `find_all` (alias `find`)
191
192
  - `find_any`
192
193
  - `find_one`
193
194
  - `find_not_all`
194
195
  - `find_none`
195
- - `first_all` (also available as `first`)
196
+ - `first_all` (alias `first`)
196
197
  - `first_any`
197
198
  - `first_one`
198
199
  - `first_not_all`
199
200
  - `first_none`
200
- - `count_all` (also available as `count`)
201
+ - `count_all` (alias `count`)
201
202
  - `count_any`
202
203
  - `count_one`
203
204
  - `count_not_all`
@@ -221,7 +222,7 @@ In other words:
221
222
 
222
223
  - `all` means `condition && condition && ...`.
223
224
  - `any` means `condition || condition || ...`.
224
- - `one` means `condition ^ condition ^ ...` (XOR).
225
+ - `one` means `condition ^ condition ^ ...`.
225
226
  - `not all` means `!(condition && condition && ...)` or `!condition || !condition || ...`.
226
227
  - `none` means `!(condition || condition || ...)` or `!condition && !condition && ...`.
227
228
 
@@ -405,6 +406,27 @@ store[1]
405
406
  # => { "id" => 1 }
406
407
  ```
407
408
 
409
+ #### Concurrent Access
410
+
411
+ To support concurrent access, e.g. when multiple threads or processes read and write the same data store file, there’s a `with_file` equivalent of `from_file` and `to_file`.
412
+
413
+ `with_file` takes a file (name) and a block. It then obtains an exclusive lock on that file, restores a data store from it or creates a new one, invokes the block with the data store and finally saves the data store back to the file. Optionally, key and/or items can be specified for when the data store is created instead of restored.
414
+
415
+ ```ruby
416
+ MemStore.with_file(file, key, items) do |store|
417
+ # create, read, update, delete
418
+ end
419
+ ```
420
+
421
+ `MemStore.with_file` is a shortcut to `MemStore::ObjectStore.with_file`.
422
+
423
+ `HashStore` offers a locking version of all its serialization methods:
424
+
425
+ - `with_file` by default,
426
+ - `with_yaml_file` when `memstore/yaml` is included,
427
+ - `with_json_file` when `memstore/json` is included,
428
+ - `with_msgpack_file` when `memstore/msgpack` is included.
429
+
408
430
  ## Contributing
409
431
 
410
432
  1. Fork it on [GitHub](https://github.com/sklppr/memstore).
@@ -1,4 +1,9 @@
1
+ # encoding: utf-8
2
+
1
3
  require "memstore/version"
4
+ require "memstore/objectstore"
5
+ require "memstore/hashstore"
6
+ require "memstore/queries"
2
7
 
3
8
  module MemStore
4
9
 
@@ -14,197 +19,8 @@ module MemStore
14
19
  ObjectStore.from_file(file)
15
20
  end
16
21
 
17
- class ObjectStore
18
-
19
- def initialize(key=nil, items={})
20
- @key = key || :hash
21
- @items = items
22
- end
23
-
24
- attr_accessor :items
25
-
26
- def insert(*items)
27
- items.each { |item| @items[key(item)] = item }
28
- self
29
- end
30
- alias_method :<<, :insert
31
-
32
- def length
33
- @items.length
34
- end
35
- alias_method :size, :length
36
- alias_method :count, :length
37
-
38
- def [](*keys)
39
- return @items[keys.first] if keys.length == 1 && !keys.first.is_a?(Range)
40
- keys.inject [] do |items, key|
41
- if key.is_a?(Range) then key.inject(items) { |i, k| i << @items[k] }
42
- else items << @items[key] end
43
- end
44
- end
45
-
46
- def all
47
- @items.values
48
- end
49
-
50
- def delete_items(*items)
51
- return @items.delete(key(items.first)) if items.length == 1
52
- items.collect { |item| @items.delete(key(item)) }
53
- end
54
- alias_method :delete_item, :delete_items
55
- alias_method :delete, :delete_items
56
-
57
- def delete_keys(*keys)
58
- return @items.delete(keys.first) if keys.length == 1 && !keys.first.is_a?(Range)
59
- keys.inject [] do |items, key|
60
- if key.is_a?(Range) then key.inject(items) { |i, k| i << @items.delete(k) }
61
- else items << @items.delete(key) end
62
- end
63
- end
64
- alias_method :delete_key, :delete_keys
65
-
66
- def find_all(conditions={}, &block)
67
- all.select { |item| instance_exec(item, conditions, block, &FIND_ALL) }
68
- end
69
- alias_method :find, :find_all
70
-
71
- def find_any(conditions={}, &block)
72
- all.select { |item| instance_exec(item, conditions, block, &FIND_ANY) }
73
- end
74
-
75
- def find_one(conditions={}, &block)
76
- all.select { |item| instance_exec(item, conditions, block, &FIND_ONE) }
77
- end
78
-
79
- def find_not_all(conditions={}, &block)
80
- all.reject { |item| instance_exec(item, conditions, block, &FIND_ALL) }
81
- end
82
-
83
- def find_none(conditions={}, &block)
84
- all.select { |item| instance_exec(item, conditions, block, &FIND_NONE) }
85
- end
86
-
87
- def first_all(conditions={}, &block)
88
- all.detect { |item| instance_exec(item, conditions, block, &FIND_ALL) }
89
- end
90
- alias_method :first, :first_all
91
-
92
- def first_any(conditions={}, &block)
93
- all.detect { |item| instance_exec(item, conditions, block, &FIND_ANY) }
94
- end
95
-
96
- def first_one(conditions={}, &block)
97
- all.detect { |item| instance_exec(item, conditions, block, &FIND_ONE) }
98
- end
99
-
100
- def first_not_all(conditions={}, &block)
101
- all.detect { |item| !instance_exec(item, conditions, block, &FIND_ALL) }
102
- end
103
-
104
- def first_none(conditions={}, &block)
105
- all.detect { |item| instance_exec(item, conditions, block, &FIND_NONE) }
106
- end
107
-
108
- def count_all(conditions={}, &block)
109
- all.count { |item| instance_exec(item, conditions, block, &FIND_ALL) }
110
- end
111
- alias_method :count, :count_all
112
-
113
- def count_any(conditions={}, &block)
114
- all.count { |item| instance_exec(item, conditions, block, &FIND_ANY) }
115
- end
116
-
117
- def count_one(conditions={}, &block)
118
- all.count { |item| instance_exec(item, conditions, block, &FIND_ONE) }
119
- end
120
-
121
- def count_not_all(conditions={}, &block)
122
- all.count { |item| !instance_exec(item, conditions, block, &FIND_ALL) }
123
- end
124
-
125
- def count_none(conditions={}, &block)
126
- all.count { |item| instance_exec(item, conditions, block, &FIND_NONE) }
127
- end
128
-
129
- def self.from_binary(binary)
130
- begin Marshal.load(binary) rescue nil end
131
- end
132
-
133
- def self.from_file(file)
134
- begin self.from_binary(IO.read(file)) rescue nil end
135
- end
136
-
137
- def to_binary
138
- Marshal.dump(self)
139
- end
140
-
141
- def to_file(file)
142
- IO.write(file, self.to_binary)
143
- end
144
-
145
- private
146
-
147
- FIND_ALL = Proc.new do |item, conditions, block|
148
- conditions.all? { |attribute, condition| condition === attr(item, attribute) } &&
149
- if block then !!block.call(item) else true end
150
- end
151
-
152
- FIND_ANY = Proc.new do |item, conditions, block|
153
- conditions.any? { |attribute, condition| condition === attr(item, attribute) } ||
154
- if block then !!block.call(item) else false end
155
- end
156
-
157
- FIND_NONE = Proc.new do |item, conditions, block|
158
- conditions.none? { |attribute, condition| condition === attr(item, attribute) } &&
159
- if block then !!block.call(item) else true end
160
- end
161
-
162
- FIND_ONE = Proc.new do |item, conditions, block|
163
- conditions.one? { |attribute, condition| condition === attr(item, attribute) } ||
164
- if block then !!block.call(item) else false end
165
- end
166
-
167
- def key(item)
168
- item.send(@key)
169
- end
170
-
171
- def attr(item, attribute)
172
- item.send(attribute)
173
- end
174
-
175
- end
176
-
177
- class HashStore < ObjectStore
178
-
179
- def self.from_hash(hash)
180
- begin
181
- key = hash[:key] || hash["key"]
182
- items = hash[:items] || hash["items"]
183
- return nil if key.nil? || items.nil?
184
- self.new(key, items)
185
- rescue
186
- nil
187
- end
188
- end
189
-
190
- def initialize(key=nil, items={})
191
- @key, @items = key, items
192
- end
193
-
194
- def to_hash
195
- { key: @key, items: @items }
196
- end
197
-
198
- private
199
-
200
- def key(item)
201
- if @key.nil? then item.hash else item[@key] end
202
- end
203
-
204
- def attr(item, attribute)
205
- item[attribute]
206
- end
207
-
22
+ def self.with_file(file, key=nil, items={}, &block)
23
+ ObjectStore.with_file(file, key, items, &block)
208
24
  end
209
25
 
210
26
  end
@@ -0,0 +1,45 @@
1
+ # encoding: utf-8
2
+
3
+ module MemStore
4
+
5
+ class HashStore < ObjectStore
6
+
7
+ def self.from_hash(hash)
8
+ begin
9
+
10
+ if hash.has_key?(:key) then key = hash[:key]
11
+ elsif hash.has_key? "key" then key = hash["key"]
12
+ else return nil end
13
+
14
+ if hash.has_key?(:items) then items = hash[:items]
15
+ elsif hash.has_key? "items" then items = hash["items"]
16
+ else return nil end
17
+
18
+ if items.is_a?(Hash) then self.new(key, items) else self.new(key) end
19
+
20
+ rescue
21
+ nil
22
+ end
23
+ end
24
+
25
+ def initialize(key=nil, items={})
26
+ @key, @items = key, items
27
+ end
28
+
29
+ def to_hash
30
+ { key: @key, items: @items }
31
+ end
32
+
33
+ private
34
+
35
+ def key(item)
36
+ if @key.nil? then item.hash else item[@key] end
37
+ end
38
+
39
+ def attr(item, attribute)
40
+ item[attribute]
41
+ end
42
+
43
+ end
44
+
45
+ end
@@ -1,3 +1,5 @@
1
+ # encoding: utf-8
2
+
1
3
  require "json"
2
4
 
3
5
  module MemStore
@@ -13,11 +15,15 @@ module MemStore
13
15
  end
14
16
 
15
17
  def self.from_json(json)
16
- begin self.from_hash(JSON.parse(json)) rescue nil end
18
+ self.from_hash(JSON.parse(json)) rescue nil
17
19
  end
18
20
 
19
21
  def self.from_json_file(file)
20
- begin self.from_json(IO.read(file)) rescue nil end
22
+ self.from_json(IO.read(file)) rescue nil
23
+ end
24
+
25
+ def self.with_json_file(file, key=nil, items={}, &block)
26
+ self.run_with_file(:from_json_file, :to_json_file, file, key, items, &block)
21
27
  end
22
28
 
23
29
  end
@@ -1,3 +1,5 @@
1
+ # encoding: utf-8
2
+
1
3
  require "msgpack"
2
4
 
3
5
  module MemStore
@@ -13,11 +15,15 @@ module MemStore
13
15
  end
14
16
 
15
17
  def self.from_msgpack(msgpack)
16
- begin self.from_hash(MessagePack.unpack(msgpack)) rescue nil end
18
+ self.from_hash(MessagePack.unpack(msgpack)) rescue nil
17
19
  end
18
20
 
19
21
  def self.from_msgpack_file(file)
20
- begin self.from_msgpack(IO.read(file)) rescue nil end
22
+ self.from_msgpack(IO.read(file)) rescue nil
23
+ end
24
+
25
+ def self.with_msgpack_file(file, key=nil, items={}, &block)
26
+ self.run_with_file(:from_msgpack_file, :to_msgpack_file, file, key, items, &block)
21
27
  end
22
28
 
23
29
  end
@@ -0,0 +1,98 @@
1
+ # encoding: utf-8
2
+
3
+ module MemStore
4
+
5
+ class ObjectStore
6
+
7
+ def initialize(key=nil, items={})
8
+ @key = key || :hash
9
+ @items = items
10
+ end
11
+
12
+ attr_accessor :items
13
+
14
+ def insert(*items)
15
+ items.each { |item| @items[key(item)] = item }
16
+ self
17
+ end
18
+ alias_method :<<, :insert
19
+
20
+ def length
21
+ @items.length
22
+ end
23
+ alias_method :size, :length
24
+ alias_method :count, :length
25
+
26
+ def [](*keys)
27
+ return @items[keys.first] if keys.length == 1 && !keys.first.is_a?(Range)
28
+ keys.inject [] do |items, key|
29
+ if key.is_a?(Range) then key.inject(items) { |i, k| i << @items[k] }
30
+ else items << @items[key] end
31
+ end
32
+ end
33
+
34
+ def all
35
+ @items.values
36
+ end
37
+
38
+ def delete_items(*items)
39
+ return @items.delete(key(items.first)) if items.length == 1
40
+ items.collect { |item| @items.delete(key(item)) }
41
+ end
42
+ alias_method :delete_item, :delete_items
43
+ alias_method :delete, :delete_items
44
+
45
+ def delete_keys(*keys)
46
+ return @items.delete(keys.first) if keys.length == 1 && !keys.first.is_a?(Range)
47
+ keys.inject [] do |items, key|
48
+ if key.is_a?(Range) then key.inject(items) { |i, k| i << @items.delete(k) }
49
+ else items << @items.delete(key) end
50
+ end
51
+ end
52
+ alias_method :delete_key, :delete_keys
53
+
54
+ def self.from_binary(binary)
55
+ restored = Marshal.load(binary) rescue nil
56
+ if restored.kind_of?(ObjectStore) || restored.kind_of?(HashStore) then restored else nil end
57
+ end
58
+
59
+ def self.from_file(file)
60
+ self.from_binary(IO.read(file)) rescue nil
61
+ end
62
+
63
+ def to_binary
64
+ Marshal.dump(self)
65
+ end
66
+
67
+ def to_file(file)
68
+ IO.write(file, self.to_binary)
69
+ end
70
+
71
+ def self.with_file(file, key=nil, items={}, &block)
72
+ self.run_with_file(:from_file, :to_file, file, key, items, &block)
73
+ end
74
+
75
+ private
76
+
77
+ def key(item)
78
+ item.send(@key)
79
+ end
80
+
81
+ def attr(item, attribute)
82
+ item.send(attribute)
83
+ end
84
+
85
+ def self.run_with_file(from_file_method, to_file_method, file, key=nil, items={}, &block)
86
+ File.open(file) do |file|
87
+ file.flock(File::LOCK_EX)
88
+ store = self.send(from_file_method, file) || self.new(key, items)
89
+ result = block.call(store)
90
+ store.send(to_file_method, file)
91
+ file.flock(File::LOCK_UN)
92
+ result
93
+ end
94
+ end
95
+
96
+ end
97
+
98
+ end
@@ -0,0 +1,94 @@
1
+ # encoding: utf-8
2
+
3
+ module MemStore
4
+
5
+ class ObjectStore
6
+
7
+ def find_all(conditions={}, &block)
8
+ all.select { |item| instance_exec(item, conditions, block, &FIND_ALL) }
9
+ end
10
+ alias_method :find, :find_all
11
+
12
+ def find_any(conditions={}, &block)
13
+ all.select { |item| instance_exec(item, conditions, block, &FIND_ANY) }
14
+ end
15
+
16
+ def find_one(conditions={}, &block)
17
+ all.select { |item| instance_exec(item, conditions, block, &FIND_ONE) }
18
+ end
19
+
20
+ def find_not_all(conditions={}, &block)
21
+ all.reject { |item| instance_exec(item, conditions, block, &FIND_ALL) }
22
+ end
23
+
24
+ def find_none(conditions={}, &block)
25
+ all.select { |item| instance_exec(item, conditions, block, &FIND_NONE) }
26
+ end
27
+
28
+ def first_all(conditions={}, &block)
29
+ all.detect { |item| instance_exec(item, conditions, block, &FIND_ALL) }
30
+ end
31
+ alias_method :first, :first_all
32
+
33
+ def first_any(conditions={}, &block)
34
+ all.detect { |item| instance_exec(item, conditions, block, &FIND_ANY) }
35
+ end
36
+
37
+ def first_one(conditions={}, &block)
38
+ all.detect { |item| instance_exec(item, conditions, block, &FIND_ONE) }
39
+ end
40
+
41
+ def first_not_all(conditions={}, &block)
42
+ all.detect { |item| !instance_exec(item, conditions, block, &FIND_ALL) }
43
+ end
44
+
45
+ def first_none(conditions={}, &block)
46
+ all.detect { |item| instance_exec(item, conditions, block, &FIND_NONE) }
47
+ end
48
+
49
+ def count_all(conditions={}, &block)
50
+ all.count { |item| instance_exec(item, conditions, block, &FIND_ALL) }
51
+ end
52
+ alias_method :count, :count_all
53
+
54
+ def count_any(conditions={}, &block)
55
+ all.count { |item| instance_exec(item, conditions, block, &FIND_ANY) }
56
+ end
57
+
58
+ def count_one(conditions={}, &block)
59
+ all.count { |item| instance_exec(item, conditions, block, &FIND_ONE) }
60
+ end
61
+
62
+ def count_not_all(conditions={}, &block)
63
+ all.count { |item| !instance_exec(item, conditions, block, &FIND_ALL) }
64
+ end
65
+
66
+ def count_none(conditions={}, &block)
67
+ all.count { |item| instance_exec(item, conditions, block, &FIND_NONE) }
68
+ end
69
+
70
+ private
71
+
72
+ FIND_ALL = Proc.new do |item, conditions, block|
73
+ conditions.all? { |attribute, condition| condition === attr(item, attribute) } &&
74
+ if block then !!block.call(item) else true end
75
+ end
76
+
77
+ FIND_ANY = Proc.new do |item, conditions, block|
78
+ conditions.any? { |attribute, condition| condition === attr(item, attribute) } ||
79
+ if block then !!block.call(item) else false end
80
+ end
81
+
82
+ FIND_NONE = Proc.new do |item, conditions, block|
83
+ conditions.none? { |attribute, condition| condition === attr(item, attribute) } &&
84
+ if block then !!block.call(item) else true end
85
+ end
86
+
87
+ FIND_ONE = Proc.new do |item, conditions, block|
88
+ conditions.one? { |attribute, condition| condition === attr(item, attribute) } ||
89
+ if block then !!block.call(item) else false end
90
+ end
91
+
92
+ end
93
+
94
+ end
@@ -1,3 +1,3 @@
1
1
  module MemStore
2
- VERSION = "1.1.0"
2
+ VERSION = "1.2.0"
3
3
  end
@@ -1,3 +1,5 @@
1
+ # encoding: utf-8
2
+
1
3
  require "yaml"
2
4
 
3
5
  module MemStore
@@ -13,11 +15,19 @@ module MemStore
13
15
  end
14
16
 
15
17
  def self.from_yaml(yaml)
16
- begin self.from_hash(YAML.load(yaml)) rescue nil end
18
+ begin
19
+ self.from_hash(YAML.load(yaml))
20
+ rescue StandardError, Psych::SyntaxError
21
+ nil
22
+ end
17
23
  end
18
24
 
19
25
  def self.from_yaml_file(file)
20
- begin self.from_yaml(IO.read(file)) rescue nil end
26
+ self.from_yaml(IO.read(file)) rescue nil
27
+ end
28
+
29
+ def self.with_yaml_file(file, key=nil, items={}, &block)
30
+ self.run_with_file(:from_yaml_file, :to_yaml_file, file, key, items, &block)
21
31
  end
22
32
 
23
33
  end
@@ -0,0 +1,58 @@
1
+ # encoding: utf-8
2
+
3
+ require "minitest/autorun"
4
+ require "tempfile"
5
+ require "memstore"
6
+
7
+ describe MemStore::HashStore do
8
+
9
+ before do
10
+ @store = MemStore::HashStore.new(:id)
11
+ 10.times { |i| @store << { id: i } }
12
+ end
13
+
14
+ it "can be instantiated with items" do
15
+ h = { a: 1, b: 2, c: 3 }
16
+ store = MemStore::HashStore.new(nil, h)
17
+ store.items.must_equal h
18
+ end
19
+
20
+ it "indexes items by Object#hash by default" do
21
+ h = {}
22
+ store = MemStore::HashStore.new.insert(h)
23
+ store.items[h.hash].must_equal h
24
+ end
25
+
26
+ it "indexes items using a custom key" do
27
+ @store.items[0].must_equal @store.all.first
28
+ end
29
+
30
+ it "can be converted to and from binary" do
31
+ restored = MemStore::HashStore.from_binary(@store.to_binary)
32
+ restored.must_be_instance_of MemStore::HashStore
33
+ restored.items.must_equal @store.items
34
+ restored.instance_variable_get(:@key).must_equal @store.instance_variable_get(:@key)
35
+ end
36
+
37
+ it "can be serialized to and deserialized from a binary file" do
38
+ tmp = Tempfile.new("memstore")
39
+ @store.to_file(tmp)
40
+ restored = MemStore::HashStore.from_file(tmp)
41
+ restored.must_be_instance_of MemStore::HashStore
42
+ restored.items.must_equal @store.items
43
+ restored.instance_variable_get(:@key).must_equal @store.instance_variable_get(:@key)
44
+ tmp.unlink
45
+ end
46
+
47
+ it "can be converted to and from a hash" do
48
+ restored = MemStore::HashStore.from_hash(@store.to_hash)
49
+ restored.items.must_equal @store.items
50
+ restored.instance_variable_get(:@key).must_equal @store.instance_variable_get(:@key)
51
+ end
52
+
53
+ it "returns nil when conversion from hash fails" do
54
+ MemStore::HashStore.from_hash(nil).must_equal nil
55
+ MemStore::HashStore.from_hash({}).must_equal nil
56
+ end
57
+
58
+ end
@@ -28,4 +28,23 @@ describe MemStore::HashStore do
28
28
  tmp.unlink
29
29
  end
30
30
 
31
+ it "returns nil when conversion from JSON fails" do
32
+ MemStore::HashStore.from_json(nil).must_equal nil
33
+ MemStore::HashStore.from_json(Marshal.dump(Object.new)).must_equal nil
34
+ end
35
+
36
+ it "supports concurrent access to a single JSON file" do
37
+ tmp = Tempfile.new("memstore")
38
+ fork do
39
+ MemStore::HashStore.with_json_file(tmp, "id") { |store| store.insert({"id"=>"1"}, {"id"=>"3"}) }
40
+ end
41
+ fork do
42
+ MemStore::HashStore.with_json_file(tmp, "id") { |store| store.insert({"id"=>"2"}, {"id"=>"4"}) }
43
+ end
44
+ sleep 0.1
45
+ restored = MemStore::HashStore.from_json_file(tmp)
46
+ restored.items.must_equal({"1"=>{"id"=>"1"}, "2"=>{"id"=>"2"}, "3"=>{"id"=>"3"}, "4"=>{"id"=>"4"}})
47
+ tmp.unlink
48
+ end
49
+
31
50
  end
@@ -28,4 +28,23 @@ describe MemStore::HashStore do
28
28
  tmp.unlink
29
29
  end
30
30
 
31
+ it "returns nil when conversion from MessagePack fails" do
32
+ MemStore::HashStore.from_msgpack(nil).must_equal nil
33
+ MemStore::HashStore.from_msgpack(Marshal.dump(Object.new)).must_equal nil
34
+ end
35
+
36
+ it "supports concurrent access to a single MessagePack file" do
37
+ tmp = Tempfile.new("memstore")
38
+ fork do
39
+ MemStore::HashStore.with_msgpack_file(tmp, "id") { |store| store.insert({"id"=>1}, {"id"=>3}) }
40
+ end
41
+ fork do
42
+ MemStore::HashStore.with_msgpack_file(tmp, "id") { |store| store.insert({"id"=>2}, {"id"=>4}) }
43
+ end
44
+ sleep 0.1
45
+ restored = MemStore::HashStore.from_msgpack_file(tmp)
46
+ restored.items.must_equal({1=>{"id"=>1}, 2=>{"id"=>2}, 3=>{"id"=>3}, 4=>{"id"=>4}})
47
+ tmp.unlink
48
+ end
49
+
31
50
  end
@@ -0,0 +1,113 @@
1
+ # encoding: utf-8
2
+
3
+ require "minitest/autorun"
4
+ require "tempfile"
5
+ require "memstore"
6
+
7
+ describe MemStore::ObjectStore do
8
+
9
+ before do
10
+ @store = MemStore::ObjectStore.new(:to_i)
11
+ 10.times { |i| @store << i.to_f }
12
+ end
13
+
14
+ it "can be instantiated with items" do
15
+ h = { a: 1, b: 2, c: 3 }
16
+ store = MemStore::ObjectStore.new(nil, h)
17
+ store.items.must_equal h
18
+ end
19
+
20
+ it "is the default when instantiating MemStore" do
21
+ MemStore.new.must_be_instance_of MemStore::ObjectStore
22
+ end
23
+
24
+ it "indexes items by Object#hash by default" do
25
+ o = Object.new
26
+ store = MemStore::ObjectStore.new.insert(o)
27
+ store.items[o.hash].must_equal o
28
+ end
29
+
30
+ it "indexes items using a custom key" do
31
+ @store.items[0].must_equal 0.0
32
+ end
33
+
34
+ it "returns the overall number of items" do
35
+ @store.length.must_equal 10
36
+ end
37
+
38
+ it "returns a single item by itself" do
39
+ @store[3].must_equal 3.0
40
+ end
41
+
42
+ it "returns multiple items as an array" do
43
+ @store[3, 4, 5, 6].must_equal [3.0, 4.0, 5.0, 6.0]
44
+ end
45
+
46
+ it "returns multiple items using a Range as an array" do
47
+ @store[0..9].must_equal @store.all
48
+ end
49
+
50
+ it "deletes a single item by reference and returns it by itself" do
51
+ @store.delete_item(3.0).must_equal 3.0
52
+ end
53
+
54
+ it "deletes multiple items by reference and returns them" do
55
+ @store.delete_items(3.0, 4.0, 5.0, 6.0).must_equal [3.0, 4.0, 5.0, 6.0]
56
+ @store.all.must_equal [0.0, 1.0, 2.0, 7.0, 8.0, 9.0]
57
+ end
58
+
59
+ it "deletes a single item by key and returns it by itself" do
60
+ @store.delete_key(3).must_equal 3.0
61
+ end
62
+
63
+ it "deletes multiple items by key and returns them as an array" do
64
+ @store.delete_keys(3, 4, 5, 6).must_equal [3.0, 4.0, 5.0, 6.0]
65
+ @store.all.must_equal [0.0, 1.0, 2.0, 7.0, 8.0, 9.0]
66
+ end
67
+
68
+ it "deletes multiple items by key using a Range and returns them as an array" do
69
+ @store.delete_keys(3..6).must_equal [3.0, 4.0, 5.0, 6.0]
70
+ @store.all.must_equal [0.0, 1.0, 2.0, 7.0, 8.0, 9.0]
71
+ end
72
+
73
+ it "can be converted to and from binary" do
74
+ restored = MemStore::ObjectStore.from_binary(@store.to_binary)
75
+ restored.must_be_instance_of MemStore::ObjectStore
76
+ restored.items.must_equal @store.items
77
+ restored.instance_variable_get(:@key).must_equal @store.instance_variable_get(:@key)
78
+ end
79
+
80
+ it "returns nil when conversion from binary fails" do
81
+ MemStore::ObjectStore.from_binary(nil).must_equal nil
82
+ MemStore::ObjectStore.from_binary(Marshal.dump(Object.new)).must_equal nil
83
+ end
84
+
85
+ it "can be serialized to and deserialized from a binary file" do
86
+ tmp = Tempfile.new("memstore")
87
+ @store.to_file(tmp)
88
+ restored = MemStore::ObjectStore.from_file(tmp)
89
+ restored.must_be_instance_of MemStore::ObjectStore
90
+ restored.items.must_equal @store.items
91
+ restored.instance_variable_get(:@key).must_equal @store.instance_variable_get(:@key)
92
+ tmp.unlink
93
+ end
94
+
95
+ it "returns nil when deserialization from binary file fails" do
96
+ MemStore::ObjectStore.from_file("does_not_exist").must_equal nil
97
+ end
98
+
99
+ it "supports concurrent access to a single binary file" do
100
+ tmp = Tempfile.new("memstore")
101
+ fork do
102
+ MemStore.with_file(tmp, :to_i) { |store| store.insert(1.0, 3.0) }
103
+ end
104
+ fork do
105
+ MemStore.with_file(tmp, :to_i) { |store| store.insert(2.0, 4.0) }
106
+ end
107
+ sleep 0.1
108
+ restored = MemStore.from_file(tmp)
109
+ restored.items.must_equal({1=>1.0, 2=>2.0, 3=>3.0, 4=>4.0})
110
+ tmp.unlink
111
+ end
112
+
113
+ end
@@ -0,0 +1,107 @@
1
+ require "minitest/autorun"
2
+ require "tempfile"
3
+
4
+ class Dummy
5
+
6
+ attr_accessor :id, :name, :child
7
+
8
+ def initialize(id, name=nil, child=nil)
9
+ @id, @name, @child = id, name, child
10
+ end
11
+
12
+ def ==(obj)
13
+ obj.is_a?(Dummy) &&
14
+ obj.id == @id &&
15
+ obj.name == @name &&
16
+ obj.child == @child
17
+ end
18
+
19
+ end
20
+
21
+ describe MemStore::ObjectStore do
22
+
23
+ before do
24
+ @store = MemStore::ObjectStore.new
25
+ strings = %w(foo moo boo faa maa baa foa moa boa lao)
26
+ classes = [String, Array]
27
+ 10.times do |i|
28
+ @store << Dummy.new(i, strings[i], classes[i%2].new)
29
+ end
30
+ end
31
+
32
+ it "finds all items fulfilling all conditions" do
33
+ matches = @store.find_all(id: 3..7, child: String)
34
+ matches.collect{ |m| m.id }.must_equal [4, 6]
35
+ end
36
+
37
+ it "finds all items fulfilling at least one condition" do
38
+ matches = @store.find_any(id: 3..7, child: String)
39
+ matches.collect{ |m| m.id }.must_equal [0, 2, 3, 4, 5, 6, 7, 8]
40
+ end
41
+
42
+ it "finds all items fulfilling exactly one condition" do
43
+ matches = @store.find_one(name: /o/, child: String)
44
+ matches.collect{ |m| m.id }.must_equal [1, 4, 7, 9]
45
+ end
46
+
47
+ it "finds all items violating at least one condition" do
48
+ matches = @store.find_not_all(name: /o/, child: String)
49
+ matches.collect{ |m| m.id }.must_equal [1, 3, 4, 5, 7, 9]
50
+ end
51
+
52
+ it "finds all items violating all conditions" do
53
+ matches = @store.find_none(name: /o/, child: String)
54
+ matches.collect{ |m| m.id }.must_equal [3, 5]
55
+ end
56
+
57
+ it "finds the first item fulfilling all conditions" do
58
+ match = @store.first_all(id: 3..7, child: String)
59
+ match.id.must_equal 4
60
+ end
61
+
62
+ it "finds the first item fulfilling at least one condition" do
63
+ match = @store.first_any(id: 3..7, child: String)
64
+ match.id.must_equal 0
65
+ end
66
+
67
+ it "finds the first item fulfilling exactly one condition" do
68
+ match = @store.first_one(name: /o/, child: String)
69
+ match.id.must_equal 1
70
+ end
71
+
72
+ it "finds the first item violating at least one condition" do
73
+ match = @store.first_not_all(name: /o/, child: String)
74
+ match.id.must_equal 1
75
+ end
76
+
77
+ it "finds the first item violating all conditions" do
78
+ match = @store.first_none(name: /o/, child: String)
79
+ match.id.must_equal 3
80
+ end
81
+
82
+ it "counts all items fulfilling all conditions" do
83
+ count = @store.count_all(id: 3..7, child: String)
84
+ count.must_equal [4, 6].length
85
+ end
86
+
87
+ it "counts all items fulfilling at least one condition" do
88
+ count = @store.count_any(id: 3..7, child: String)
89
+ count.must_equal [0, 2, 3, 4, 5, 6, 7, 8].length
90
+ end
91
+
92
+ it "counts all items fulfilling exactly one condition" do
93
+ count = @store.count_one(name: /o/, child: String)
94
+ count.must_equal [1, 4, 7, 9].length
95
+ end
96
+
97
+ it "counts all items violating at least one condition" do
98
+ count = @store.count_not_all(name: /o/, child: String)
99
+ count.must_equal [1, 3, 4, 5, 7, 9].length
100
+ end
101
+
102
+ it "counts all items violating all conditions" do
103
+ count = @store.count_none(name: /o/, child: String)
104
+ count.must_equal [3, 5].length
105
+ end
106
+
107
+ end
@@ -28,4 +28,23 @@ describe MemStore::HashStore do
28
28
  tmp.unlink
29
29
  end
30
30
 
31
+ it "returns nil when conversion from YAML fails" do
32
+ MemStore::HashStore.from_yaml(nil).must_equal nil
33
+ MemStore::HashStore.from_yaml(Marshal.dump(Object.new)).must_equal nil
34
+ end
35
+
36
+ it "supports concurrent access to a single YAML file" do
37
+ tmp = Tempfile.new("memstore")
38
+ fork do
39
+ MemStore::HashStore.with_yaml_file(tmp, :id) { |store| store.insert({id: 1}, {id: 3}) }
40
+ end
41
+ fork do
42
+ MemStore::HashStore.with_yaml_file(tmp, :id) { |store| store.insert({id: 2}, {id: 4}) }
43
+ end
44
+ sleep 0.1
45
+ restored = MemStore::HashStore.from_yaml_file(tmp)
46
+ restored.items.must_equal({1=>{id: 1}, 2=>{id: 2}, 3=>{id: 3}, 4=>{id: 4}})
47
+ tmp.unlink
48
+ end
49
+
31
50
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: memstore
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.2.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -25,15 +25,20 @@ files:
25
25
  - README.md
26
26
  - Rakefile
27
27
  - lib/memstore.rb
28
+ - lib/memstore/hashstore.rb
28
29
  - lib/memstore/json.rb
29
30
  - lib/memstore/msgpack.rb
31
+ - lib/memstore/objectstore.rb
32
+ - lib/memstore/queries.rb
30
33
  - lib/memstore/version.rb
31
34
  - lib/memstore/yaml.rb
32
35
  - memstore.gemspec
33
- - spec/memstore_json_spec.rb
34
- - spec/memstore_msgpack_spec.rb
35
- - spec/memstore_spec.rb
36
- - spec/memstore_yaml_spec.rb
36
+ - spec/hashstore_spec.rb
37
+ - spec/json_spec.rb
38
+ - spec/msgpack_spec.rb
39
+ - spec/objectstore_spec.rb
40
+ - spec/queries_spec.rb
41
+ - spec/yaml_spec.rb
37
42
  homepage: https://github.com/sklppr/memstore
38
43
  licenses: []
39
44
  post_install_message:
@@ -59,7 +64,9 @@ signing_key:
59
64
  specification_version: 3
60
65
  summary: A simple, in-memory data store.
61
66
  test_files:
62
- - spec/memstore_json_spec.rb
63
- - spec/memstore_msgpack_spec.rb
64
- - spec/memstore_spec.rb
65
- - spec/memstore_yaml_spec.rb
67
+ - spec/hashstore_spec.rb
68
+ - spec/json_spec.rb
69
+ - spec/msgpack_spec.rb
70
+ - spec/objectstore_spec.rb
71
+ - spec/queries_spec.rb
72
+ - spec/yaml_spec.rb
@@ -1,268 +0,0 @@
1
- # encoding: utf-8
2
-
3
- require "minitest/autorun"
4
- require "tempfile"
5
- require "memstore"
6
-
7
- class Dummy
8
-
9
- attr_accessor :id, :name, :child
10
-
11
- def initialize(id, name=nil, child=nil)
12
- @id, @name, @child = id, name, child
13
- end
14
-
15
- def ==(obj)
16
- obj.is_a?(Dummy) &&
17
- obj.id == @id &&
18
- obj.name == @name &&
19
- obj.child == @child
20
- end
21
-
22
- end
23
-
24
- describe MemStore::ObjectStore do
25
-
26
- it "can be instantiated with items" do
27
- h = { a: 1, b: 2, c: 3 }
28
- store = MemStore::ObjectStore.new(nil, h)
29
- store.items.must_equal h
30
- end
31
-
32
- it "is the default when instantiating MemStore" do
33
- MemStore.new.must_be_instance_of MemStore::ObjectStore
34
- end
35
-
36
- it "indexes items by Object#hash by default" do
37
- o = Object.new
38
- store = MemStore::ObjectStore.new.insert(o)
39
- store.items[o.hash].must_equal o
40
- end
41
-
42
- it "indexes items using a custom key" do
43
- o = Dummy.new("custom key")
44
- store = MemStore::ObjectStore.new(:id).insert(o)
45
- store.items[o.id].must_equal o
46
- end
47
-
48
- it "can be converted to and from binary" do
49
- o = Dummy.new("custom key")
50
- store = MemStore::ObjectStore.new(:id).insert(o)
51
- restored = MemStore::ObjectStore.from_binary(store.to_binary)
52
- restored.must_be_instance_of MemStore::ObjectStore
53
- restored.items.must_equal store.items
54
- restored.instance_variable_get(:@key).must_equal :id
55
- end
56
-
57
- it "returns nil when conversion from binary fails" do
58
- MemStore::ObjectStore.from_binary(nil).must_equal nil
59
- end
60
-
61
- it "can be serialized to and deserialized from a binary file" do
62
- tmp = Tempfile.new("memstore")
63
- o = Dummy.new("custom key")
64
- store = MemStore::ObjectStore.new(:id).insert(o)
65
- store.to_file(tmp)
66
- restored = MemStore::ObjectStore.from_file(tmp)
67
- restored.must_be_instance_of MemStore::ObjectStore
68
- restored.items.must_equal store.items
69
- restored.instance_variable_get(:@key).must_equal :id
70
- tmp.unlink
71
- end
72
-
73
- it "returns nil when deserialization from binary file fails" do
74
- MemStore::ObjectStore.from_file("does_not_exist").must_equal nil
75
- end
76
-
77
- end
78
-
79
- describe MemStore::HashStore do
80
-
81
- it "can be instantiated with items" do
82
- h = { a: 1, b: 2, c: 3 }
83
- store = MemStore::HashStore.new(nil, h)
84
- store.items.must_equal h
85
- end
86
-
87
- it "indexes items by Object#hash by default" do
88
- h = {}
89
- store = MemStore::HashStore.new.insert(h)
90
- store.items[h.hash].must_equal h
91
- end
92
-
93
- it "indexes items using a custom key" do
94
- h = { id: "custom key" }
95
- store = MemStore::HashStore.new(:id).insert(h)
96
- store.items[h[:id]].must_equal h
97
- end
98
-
99
- it "can be converted to and from binary" do
100
- store = MemStore::HashStore.new(:id).insert({ id: "custom key" })
101
- restored = MemStore::HashStore.from_binary(store.to_binary)
102
- restored.must_be_instance_of MemStore::HashStore
103
- restored.items.must_equal store.items
104
- restored.instance_variable_get(:@key).must_equal :id
105
- end
106
-
107
- it "can be serialized to and deserialized from a binary file" do
108
- tmp = Tempfile.new("memstore")
109
- store = MemStore::HashStore.new(:id).insert({ id: "custom key" })
110
- store.to_file(tmp)
111
- restored = MemStore::HashStore.from_file(tmp)
112
- restored.must_be_instance_of MemStore::HashStore
113
- restored.items.must_equal store.items
114
- restored.instance_variable_get(:@key).must_equal :id
115
- tmp.unlink
116
- end
117
-
118
- it "can be converted to and from a hash" do
119
- store = MemStore::HashStore.new(:id)
120
- 10.times { |i| store << { id: i } }
121
- restored = MemStore::HashStore.from_hash(store.to_hash)
122
- restored.items.must_equal store.items
123
- restored.instance_variable_get(:@key).must_equal :id
124
- end
125
-
126
- it "returns nil when conversion from hash fails" do
127
- MemStore::HashStore.from_hash(nil).must_equal nil
128
- MemStore::HashStore.from_hash({}).must_equal nil
129
- end
130
-
131
- end
132
-
133
- describe MemStore do
134
-
135
- before do
136
- @store = MemStore.new(:to_i)
137
- # Use float as objects and integer as key
138
- 10.times { |i| @store << i.to_f }
139
- end
140
-
141
- it "returns the overall number of items" do
142
- @store.length.must_equal 10
143
- end
144
-
145
- it "returns a single item by itself" do
146
- @store[3].must_equal 3.0
147
- end
148
-
149
- it "returns multiple items as an array" do
150
- @store[3, 4, 5, 6].must_equal [3.0, 4.0, 5.0, 6.0]
151
- end
152
-
153
- it "returns multiple items using a Range as an array" do
154
- @store[0..9].must_equal @store.all
155
- end
156
-
157
- it "deletes a single item by reference and returns it by itself" do
158
- @store.delete_item(3.0).must_equal 3.0
159
- end
160
-
161
- it "deletes multiple items by reference and returns them" do
162
- @store.delete_items(3.0, 4.0, 5.0, 6.0).must_equal [3.0, 4.0, 5.0, 6.0]
163
- @store.all.must_equal [0.0, 1.0, 2.0, 7.0, 8.0, 9.0]
164
- end
165
-
166
- it "deletes a single item by key and returns it by itself" do
167
- @store.delete_key(3).must_equal 3.0
168
- end
169
-
170
- it "deletes multiple items by key and returns them as an array" do
171
- @store.delete_keys(3, 4, 5, 6).must_equal [3.0, 4.0, 5.0, 6.0]
172
- @store.all.must_equal [0.0, 1.0, 2.0, 7.0, 8.0, 9.0]
173
- end
174
-
175
- it "deletes multiple items by key using a Range and returns them as an array" do
176
- @store.delete_keys(3..6).must_equal [3.0, 4.0, 5.0, 6.0]
177
- @store.all.must_equal [0.0, 1.0, 2.0, 7.0, 8.0, 9.0]
178
- end
179
-
180
- end
181
-
182
- describe MemStore do
183
-
184
- before do
185
- @store = MemStore.new
186
- strings = %w(foo moo boo faa maa baa foa moa boa lao)
187
- classes = [String, Array]
188
- 10.times do |i|
189
- @store << Dummy.new(i, strings[i], classes[i%2].new)
190
- end
191
- end
192
-
193
- it "finds all items fulfilling all conditions" do
194
- matches = @store.find_all(id: 3..7, child: String)
195
- matches.collect{ |m| m.id }.must_equal [4, 6]
196
- end
197
-
198
- it "finds all items fulfilling at least one condition" do
199
- matches = @store.find_any(id: 3..7, child: String)
200
- matches.collect{ |m| m.id }.must_equal [0, 2, 3, 4, 5, 6, 7, 8]
201
- end
202
-
203
- it "finds all items fulfilling exactly one condition" do
204
- matches = @store.find_one(name: /o/, child: String)
205
- matches.collect{ |m| m.id }.must_equal [1, 4, 7, 9]
206
- end
207
-
208
- it "finds all items violating at least one condition" do
209
- matches = @store.find_not_all(name: /o/, child: String)
210
- matches.collect{ |m| m.id }.must_equal [1, 3, 4, 5, 7, 9]
211
- end
212
-
213
- it "finds all items violating all conditions" do
214
- matches = @store.find_none(name: /o/, child: String)
215
- matches.collect{ |m| m.id }.must_equal [3, 5]
216
- end
217
-
218
- it "finds the first item fulfilling all conditions" do
219
- match = @store.first_all(id: 3..7, child: String)
220
- match.id.must_equal 4
221
- end
222
-
223
- it "finds the first item fulfilling at least one condition" do
224
- match = @store.first_any(id: 3..7, child: String)
225
- match.id.must_equal 0
226
- end
227
-
228
- it "finds the first item fulfilling exactly one condition" do
229
- match = @store.first_one(name: /o/, child: String)
230
- match.id.must_equal 1
231
- end
232
-
233
- it "finds the first item violating at least one condition" do
234
- match = @store.first_not_all(name: /o/, child: String)
235
- match.id.must_equal 1
236
- end
237
-
238
- it "finds the first item violating all conditions" do
239
- match = @store.first_none(name: /o/, child: String)
240
- match.id.must_equal 3
241
- end
242
-
243
- it "counts all items fulfilling all conditions" do
244
- count = @store.count_all(id: 3..7, child: String)
245
- count.must_equal [4, 6].length
246
- end
247
-
248
- it "counts all items fulfilling at least one condition" do
249
- count = @store.count_any(id: 3..7, child: String)
250
- count.must_equal [0, 2, 3, 4, 5, 6, 7, 8].length
251
- end
252
-
253
- it "counts all items fulfilling exactly one condition" do
254
- count = @store.count_one(name: /o/, child: String)
255
- count.must_equal [1, 4, 7, 9].length
256
- end
257
-
258
- it "counts all items violating at least one condition" do
259
- count = @store.count_not_all(name: /o/, child: String)
260
- count.must_equal [1, 3, 4, 5, 7, 9].length
261
- end
262
-
263
- it "counts all items violating all conditions" do
264
- count = @store.count_none(name: /o/, child: String)
265
- count.must_equal [3, 5].length
266
- end
267
-
268
- end