memstore 1.1.0 → 1.2.0

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.
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