memstore 1.0.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/.gitignore +17 -0
- data/Gemfile +3 -0
- data/LICENSE.txt +22 -0
- data/README.md +391 -0
- data/Rakefile +6 -0
- data/lib/memstore.rb +164 -0
- data/lib/memstore/json.rb +25 -0
- data/lib/memstore/msgpack.rb +25 -0
- data/lib/memstore/version.rb +3 -0
- data/lib/memstore/yaml.rb +25 -0
- data/memstore.gemspec +18 -0
- data/spec/memstore_json_spec.rb +31 -0
- data/spec/memstore_msgpack_spec.rb +31 -0
- data/spec/memstore_spec.rb +192 -0
- data/spec/memstore_yaml_spec.rb +31 -0
- metadata +65 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Sebastian Klepper
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,391 @@
|
|
1
|
+
# MemStore
|
2
|
+
|
3
|
+
*A simple in-memory data store.*
|
4
|
+
|
5
|
+
MemStore is a simple in-memory data store that supports adding, retrieving and deleting items as well as complex search queries and easy serialization.
|
6
|
+
|
7
|
+
It’s not in any way supposed to be a “real” database. However, it can replace a database in small applications or prototypes.
|
8
|
+
|
9
|
+
## Installation
|
10
|
+
|
11
|
+
Add this line to your application's Gemfile:
|
12
|
+
|
13
|
+
gem "memstore"
|
14
|
+
|
15
|
+
And then execute:
|
16
|
+
|
17
|
+
$ bundle
|
18
|
+
|
19
|
+
Or install it yourself as:
|
20
|
+
|
21
|
+
$ gem install memstore
|
22
|
+
|
23
|
+
## Usage
|
24
|
+
|
25
|
+
- [Basics](#basics)
|
26
|
+
- [Objects vs. Hashes](#objects-vs-hashes)
|
27
|
+
- [Adding Items](#adding-items)
|
28
|
+
- [Retrieving Items](#retrieving-items)
|
29
|
+
- [Deleting Items](#deleting-items)
|
30
|
+
- [Search Queries](#search-queries)
|
31
|
+
- [Serialization](#serialization)
|
32
|
+
- [Binary](#binary)
|
33
|
+
- [Hash](#hash)
|
34
|
+
- [YAML](#yaml)
|
35
|
+
- [JSON](#json)
|
36
|
+
- [MessagePack](#messagepack)
|
37
|
+
|
38
|
+
### Basics
|
39
|
+
|
40
|
+
Creating a data store is utterly simple:
|
41
|
+
|
42
|
+
```ruby
|
43
|
+
mb = MemStore.new
|
44
|
+
```
|
45
|
+
|
46
|
+
By default, objects are indexed using `Object#hash`.
|
47
|
+
|
48
|
+
If a different property should be used, it can be specified like this:
|
49
|
+
|
50
|
+
```ruby
|
51
|
+
mb = MemStore.new(:id)
|
52
|
+
```
|
53
|
+
|
54
|
+
The property needs to be truly unique for all objects since it’s used as a hash key internally.
|
55
|
+
|
56
|
+
An items collection can also be provided on creation:
|
57
|
+
|
58
|
+
```ruby
|
59
|
+
mb = MemStore.new(nil, { ... }) # to use Object.hash as key
|
60
|
+
mb = MemStore.new(:id, { ... }) # to use custom key
|
61
|
+
```
|
62
|
+
|
63
|
+
The collection must be a hash that correctly maps the used key to each item.
|
64
|
+
|
65
|
+
### Objects vs. Hashes
|
66
|
+
|
67
|
+
MemStore comes in two flavors: `ObjectStore` and `HashStore`.
|
68
|
+
|
69
|
+
They’re basically the same, but `ObjectStore` accesses items through `item.attribute` while `HashStore` accesses items through `item[attribute]`.
|
70
|
+
|
71
|
+
`ObjectStore` is the default variant:
|
72
|
+
|
73
|
+
```ruby
|
74
|
+
mb = MemStore.new
|
75
|
+
# is equal to
|
76
|
+
mb = MemStore::ObjectStore.new
|
77
|
+
```
|
78
|
+
|
79
|
+
`HashStore` needs to be created explicitly:
|
80
|
+
|
81
|
+
```ruby
|
82
|
+
mb = MemStore::HashStore.new
|
83
|
+
```
|
84
|
+
|
85
|
+
If no key attribute is specified, `HashStore` will also use `Object#hash`.
|
86
|
+
|
87
|
+
### Adding Items
|
88
|
+
|
89
|
+
`items` provides direct access to the internal items hash.
|
90
|
+
|
91
|
+
```ruby
|
92
|
+
mb.items
|
93
|
+
# => {}
|
94
|
+
mb.items = { 1 => a, 2 => b, 3 => c }
|
95
|
+
# => { 1 => a, 2 => b, 3 => c }
|
96
|
+
```
|
97
|
+
|
98
|
+
`insert` adds one or multiple items and returns the data store itself:
|
99
|
+
|
100
|
+
```ruby
|
101
|
+
mb.insert(a, b, c)
|
102
|
+
# => mb
|
103
|
+
```
|
104
|
+
|
105
|
+
Since it returns the data store, items can be added right after instantiation:
|
106
|
+
|
107
|
+
```ruby
|
108
|
+
mb = MemStore.new.insert(a, b, c)
|
109
|
+
# => mb
|
110
|
+
```
|
111
|
+
|
112
|
+
MemStore also supports the shovel operator `<<` for adding items.
|
113
|
+
Only one item can be added at a time but it’s chainable:
|
114
|
+
|
115
|
+
```ruby
|
116
|
+
mb << a << b << c
|
117
|
+
# => mb
|
118
|
+
```
|
119
|
+
|
120
|
+
### Retrieving Items
|
121
|
+
|
122
|
+
`size` returns the current number of items:
|
123
|
+
|
124
|
+
```ruby
|
125
|
+
mb.size
|
126
|
+
# => 3
|
127
|
+
```
|
128
|
+
|
129
|
+
The bracket operator `[]` is used to look up items by their key.
|
130
|
+
If a single key is given, a single item will be returned.
|
131
|
+
If multiple keys are given, an array of items will be returned with `nil` when there is no item for a key.
|
132
|
+
|
133
|
+
```ruby
|
134
|
+
mb[1]
|
135
|
+
# => a
|
136
|
+
mb[1, 2, 3]
|
137
|
+
# => [a, b, c]
|
138
|
+
```
|
139
|
+
|
140
|
+
Ranges are also supported and can even be combined with single keys:
|
141
|
+
|
142
|
+
```ruby
|
143
|
+
mb[1..3]
|
144
|
+
# => [a, b, c]
|
145
|
+
mb[1..3, 6]
|
146
|
+
# => [a, b, c, f]
|
147
|
+
```
|
148
|
+
|
149
|
+
### Deleting Items
|
150
|
+
|
151
|
+
`delete_items` (or `delete_item`) deletes items by reference and returns them.
|
152
|
+
This is considered the default use case and therefore also available as `delete`.
|
153
|
+
|
154
|
+
If one item is given, it is deleted and returned.
|
155
|
+
If multiple items are given, they are deleted and returned as an array.
|
156
|
+
|
157
|
+
```ruby
|
158
|
+
mb.delete_item(a)
|
159
|
+
# => a
|
160
|
+
mb.delete_items(b, c, d)
|
161
|
+
# => [b, c, d]
|
162
|
+
mb.delete(e, f, g)
|
163
|
+
# => [e, f, g]
|
164
|
+
```
|
165
|
+
|
166
|
+
This is considered the default use case and therefore also available as `delete`.
|
167
|
+
|
168
|
+
`delete_keys` (or `delete_key`) deletes items by key and returns them.
|
169
|
+
Again, one or multiple items can be deleted at a time and even ranges are handled.
|
170
|
+
|
171
|
+
```ruby
|
172
|
+
mb.delete_key(1)
|
173
|
+
# => a
|
174
|
+
mb.delete_keys(2, 3, 4)
|
175
|
+
# => [b, c, d]
|
176
|
+
mb.delete_keys(5..7, 9)
|
177
|
+
# => [e, f, g, i]
|
178
|
+
```
|
179
|
+
|
180
|
+
### Search Queries
|
181
|
+
|
182
|
+
The following methods are available to query the data store:
|
183
|
+
|
184
|
+
- `find_all` (`find`)
|
185
|
+
- `find_any`
|
186
|
+
- `find_one`
|
187
|
+
- `find_not_all`
|
188
|
+
- `find_none`
|
189
|
+
- `first_all` (`first`)
|
190
|
+
- `first_any`
|
191
|
+
- `first_one`
|
192
|
+
- `first_not_all`
|
193
|
+
- `first_none`
|
194
|
+
|
195
|
+
The first part indicates what is returned:
|
196
|
+
|
197
|
+
- `find_*` returns all matches.
|
198
|
+
- `first_*` returns the first match.
|
199
|
+
|
200
|
+
The second part indicates how conditions are evaluated:
|
201
|
+
|
202
|
+
- `*_all` matches items *fulfilling all* conditions.
|
203
|
+
- `*_any` matches items *fulfilling at least one* condition.
|
204
|
+
- `*_one` matches items *fulfilling exactly one* condition.
|
205
|
+
- `*_not_all` matches items *violating at least one* condition.
|
206
|
+
- `*_none` matches items *violating all* conditions.
|
207
|
+
|
208
|
+
In other words:
|
209
|
+
|
210
|
+
- `all` means `condition && condition && ...`
|
211
|
+
- `any` means `condition || condition || ...`
|
212
|
+
- `one` means `condition ^ condition ^ ...` (XOR)
|
213
|
+
- `not all` means `!(condition && condition && ...)` or `!condition || !condition || ...`
|
214
|
+
- `none` means `!(condition || condition || ...)` or `!condition && !condition && ...`
|
215
|
+
|
216
|
+
For convenience, `find` is aliased to `find_all` and `first` to `first_all`.
|
217
|
+
|
218
|
+
All variants take a `conditions` hash and an optional block.
|
219
|
+
|
220
|
+
The hash maps attributes names to conditions that should be tested.
|
221
|
+
Conditions are evaluated using the `===` operator and can be virtually anything:
|
222
|
+
|
223
|
+
```ruby
|
224
|
+
mb.find(name: "Fred", age: 25)
|
225
|
+
mb.find(name: /red/i, age: 10..30)
|
226
|
+
mb.find(child: MyClass)
|
227
|
+
mb.find(child: -> child { child.valid? })
|
228
|
+
```
|
229
|
+
|
230
|
+
Additional types can be used in conditions by supporting the `===` operator. For example:
|
231
|
+
|
232
|
+
```ruby
|
233
|
+
class Array
|
234
|
+
def ===(obj)
|
235
|
+
self.include?(obj)
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
mb.find age: [23, 25, 27]
|
240
|
+
```
|
241
|
+
|
242
|
+
The block is invoked with every item and can do more complex tests.
|
243
|
+
Its return value is interpreted as a boolean value:
|
244
|
+
|
245
|
+
```ruby
|
246
|
+
mb.find { |item| item.age - item.child.age > 20 }
|
247
|
+
```
|
248
|
+
|
249
|
+
In addition to the evaluation logic, the arrays returned by all variants of `find` can be merged:
|
250
|
+
|
251
|
+
```ruby
|
252
|
+
mb.find(...) | mb.find(...) | mb.find(...)
|
253
|
+
```
|
254
|
+
|
255
|
+
Note that the pipe operator `|` already eliminates duplicates:
|
256
|
+
|
257
|
+
```ruby
|
258
|
+
[a, b, c] | [c, d, e]
|
259
|
+
# => [a, b, c, d, e]
|
260
|
+
# which is equal to
|
261
|
+
([a, b, c] + [c, d, e]).uniq
|
262
|
+
```
|
263
|
+
|
264
|
+
### Serialization
|
265
|
+
|
266
|
+
#### Binary
|
267
|
+
|
268
|
+
The data store can easily be serialized and restored in binary format:
|
269
|
+
|
270
|
+
```ruby
|
271
|
+
mb.to_file("datastore.bin")
|
272
|
+
# => number of bytes written
|
273
|
+
MemStore.from_file("datastore.bin")
|
274
|
+
# => instance of ObjectStore or HashStore
|
275
|
+
```
|
276
|
+
|
277
|
+
MemStore will automatically restore the correct class (`ObjectStore`/`HashStore`), key and items.
|
278
|
+
|
279
|
+
#### Hash
|
280
|
+
|
281
|
+
`HashStore` can be converted to and from a hash:
|
282
|
+
|
283
|
+
```ruby
|
284
|
+
h = mb.to_hash
|
285
|
+
# => { key: ..., items: { ... } }
|
286
|
+
MemStore::HashStore.from_hash(h)
|
287
|
+
# => instance of HashStore
|
288
|
+
```
|
289
|
+
|
290
|
+
#### YAML
|
291
|
+
|
292
|
+
`memstore/yaml` enables serialization of `HashStore` to and from [YAML](http://yaml.org/):
|
293
|
+
|
294
|
+
```ruby
|
295
|
+
require "memstore/yaml" # requires "yaml"
|
296
|
+
|
297
|
+
mb.to_yaml
|
298
|
+
# => YAML string
|
299
|
+
mb.to_yaml_file(file)
|
300
|
+
# => number of bytes written
|
301
|
+
MemStore::HashStore.from_yaml(yaml)
|
302
|
+
# => instance of HashStore
|
303
|
+
MemStore::HashStore.from_yaml_file(file)
|
304
|
+
# => instance of HashStore
|
305
|
+
```
|
306
|
+
|
307
|
+
De/serialization is seamless since YAML can handle symbols and non-string keys (i.e. Psych converts them correctly).
|
308
|
+
|
309
|
+
#### JSON
|
310
|
+
|
311
|
+
`memstore/json` enables serialization of `HashStore` to and from [JSON](http://www.json.org/):
|
312
|
+
|
313
|
+
```ruby
|
314
|
+
require "memstore/json" # requires "json"
|
315
|
+
|
316
|
+
mb.to_json
|
317
|
+
# => JSON string
|
318
|
+
mb.to_json_file(file)
|
319
|
+
# => number of bytes written
|
320
|
+
MemStore::HashStore.from_json(json)
|
321
|
+
# => instance of HashStore
|
322
|
+
MemStore::HashStore.from_json_file(file)
|
323
|
+
# => instance of HashStore
|
324
|
+
```
|
325
|
+
|
326
|
+
**Important:** Symbols will be converted to strings and JSON only allows string keys.
|
327
|
+
|
328
|
+
```ruby
|
329
|
+
mb = MemStore::HashStore.new(:id)
|
330
|
+
mb << { id: 1 }
|
331
|
+
mb.to_hash
|
332
|
+
# => { :key => :id, :items => { 1 => { :id => 1 } } }
|
333
|
+
mb = MemStore::HashStore.from_json(mb.to_json)
|
334
|
+
mb.to_hash
|
335
|
+
# => { :key => "id", :items => { "1" => { "id" => 1 } } }
|
336
|
+
```
|
337
|
+
|
338
|
+
The following style ensures consistent access before and after serialization:
|
339
|
+
|
340
|
+
```ruby
|
341
|
+
mb = MemStore::HashStore.new("id")
|
342
|
+
mb << { "id" => "1" }
|
343
|
+
mb["1"]
|
344
|
+
# => { "id" => "1" }
|
345
|
+
```
|
346
|
+
|
347
|
+
#### MessagePack
|
348
|
+
|
349
|
+
`memstore/msgpack` enables serialization of `HashStore` to and from [MessagePack](http://msgpack.org/):
|
350
|
+
|
351
|
+
```ruby
|
352
|
+
require "memstore/msgpack" # requires "msgpack"
|
353
|
+
|
354
|
+
mb.to_msgpack
|
355
|
+
# => MessagePack binary format
|
356
|
+
mb.to_msgpack_file(file)
|
357
|
+
# => number of bytes written
|
358
|
+
MemStore::HashStore.from_msgpack(msgpack)
|
359
|
+
# => instance of HashStore
|
360
|
+
MemStore::HashStore.from_msgpack_file(file)
|
361
|
+
# => instance of HashStore
|
362
|
+
```
|
363
|
+
|
364
|
+
**Important:** Symbols will be converted to strings but non-string keys are allowed.
|
365
|
+
|
366
|
+
```ruby
|
367
|
+
mb = MemStore::HashStore.new(:id)
|
368
|
+
mb << { id: 1 }
|
369
|
+
mb.to_hash
|
370
|
+
# => { :key => :id, :items => { 1 => { :id => 1 } } }
|
371
|
+
mb = MemStore::HashStore.from_msgpack(mb.to_msgpack)
|
372
|
+
mb.to_hash
|
373
|
+
# => { :key => "id", :items => { 1 => { "id" => 1 } } }
|
374
|
+
```
|
375
|
+
|
376
|
+
The following style ensures consistent access before and after serialization:
|
377
|
+
|
378
|
+
```ruby
|
379
|
+
mb = MemStore::HashStore.new("id")
|
380
|
+
mb << { "id" => 1 }
|
381
|
+
mb[1]
|
382
|
+
# => { "id" => 1 }
|
383
|
+
```
|
384
|
+
|
385
|
+
## Contributing
|
386
|
+
|
387
|
+
1. Fork it
|
388
|
+
2. Create your feature branch: `git checkout -b my-new-feature`
|
389
|
+
3. Commit your changes: `git commit -am 'Add some feature'`
|
390
|
+
4. Push to the branch: `git push origin my-new-feature`
|
391
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
data/lib/memstore.rb
ADDED
@@ -0,0 +1,164 @@
|
|
1
|
+
require "memstore/version"
|
2
|
+
|
3
|
+
module MemStore
|
4
|
+
|
5
|
+
def self.new(key=nil)
|
6
|
+
ObjectStore.new(key)
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.from_file(file)
|
10
|
+
Marshal.load IO.read(file)
|
11
|
+
end
|
12
|
+
|
13
|
+
class ObjectStore
|
14
|
+
|
15
|
+
def initialize(key=nil, items={})
|
16
|
+
@key = key || :hash
|
17
|
+
@items = items
|
18
|
+
end
|
19
|
+
|
20
|
+
attr_accessor :items
|
21
|
+
|
22
|
+
def insert(*items)
|
23
|
+
items.each { |item| @items[key(item)] = item }
|
24
|
+
self
|
25
|
+
end
|
26
|
+
alias_method :<<, :insert
|
27
|
+
|
28
|
+
def size
|
29
|
+
@items.length
|
30
|
+
end
|
31
|
+
|
32
|
+
def [](*keys)
|
33
|
+
return @items[keys.first] if keys.length == 1 && !keys.first.is_a?(Range)
|
34
|
+
keys.inject [] do |items, key|
|
35
|
+
if key.is_a? Range then key.inject(items) { |i, k| i << @items[k] }
|
36
|
+
else items << @items[key] end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def all
|
41
|
+
@items.values
|
42
|
+
end
|
43
|
+
|
44
|
+
def delete_items(*items)
|
45
|
+
return @items.delete(key(items.first)) if items.length == 1
|
46
|
+
items.collect { |item| @items.delete(key(item)) }
|
47
|
+
end
|
48
|
+
alias_method :delete_item, :delete_items
|
49
|
+
alias_method :delete, :delete_items
|
50
|
+
|
51
|
+
def delete_keys(*keys)
|
52
|
+
return @items.delete(keys.first) if keys.length == 1 && !keys.first.is_a?(Range)
|
53
|
+
keys.inject [] do |items, key|
|
54
|
+
if key.is_a? Range then key.inject(items) { |i, k| i << @items.delete(k) }
|
55
|
+
else items << @items.delete(key) end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
alias_method :delete_key, :delete_keys
|
59
|
+
|
60
|
+
def find_all(conditions={}, &block)
|
61
|
+
all.select { |item| instance_exec(item, conditions, block, &FIND_ALL) }
|
62
|
+
end
|
63
|
+
alias_method :find, :find_all
|
64
|
+
|
65
|
+
def find_any(conditions={}, &block)
|
66
|
+
all.select { |item| instance_exec(item, conditions, block, &FIND_ANY) }
|
67
|
+
end
|
68
|
+
|
69
|
+
def find_one(conditions={}, &block)
|
70
|
+
all.select { |item| instance_exec(item, conditions, block, &FIND_ONE) }
|
71
|
+
end
|
72
|
+
|
73
|
+
def find_not_all(conditions={}, &block)
|
74
|
+
all.reject { |item| instance_exec(item, conditions, block, &FIND_ALL) }
|
75
|
+
end
|
76
|
+
|
77
|
+
def find_none(conditions={}, &block)
|
78
|
+
all.select { |item| instance_exec(item, conditions, block, &FIND_NONE) }
|
79
|
+
end
|
80
|
+
|
81
|
+
def first_all(conditions={}, &block)
|
82
|
+
all.detect { |item| instance_exec(item, conditions, block, &FIND_ALL) }
|
83
|
+
end
|
84
|
+
alias_method :first, :first_all
|
85
|
+
|
86
|
+
def first_any(conditions={}, &block)
|
87
|
+
all.detect { |item| instance_exec(item, conditions, block, &FIND_ANY) }
|
88
|
+
end
|
89
|
+
|
90
|
+
def first_one(conditions={}, &block)
|
91
|
+
all.detect { |item| instance_exec(item, conditions, block, &FIND_ONE) }
|
92
|
+
end
|
93
|
+
|
94
|
+
def first_not_all(conditions={}, &block)
|
95
|
+
all.detect { |item| !instance_exec(item, conditions, block, &FIND_ALL) }
|
96
|
+
end
|
97
|
+
|
98
|
+
def first_none(conditions={}, &block)
|
99
|
+
all.detect { |item| instance_exec(item, conditions, block, &FIND_NONE) }
|
100
|
+
end
|
101
|
+
|
102
|
+
def to_file(file)
|
103
|
+
IO.write file, Marshal.dump(self)
|
104
|
+
end
|
105
|
+
|
106
|
+
private
|
107
|
+
|
108
|
+
FIND_ALL = Proc.new do |item, conditions, block|
|
109
|
+
conditions.all? { |attribute, condition| condition === attr(item, attribute) } &&
|
110
|
+
if block then !!block.call(item) else true end
|
111
|
+
end
|
112
|
+
|
113
|
+
FIND_ANY = Proc.new do |item, conditions, block|
|
114
|
+
conditions.any? { |attribute, condition| condition === attr(item, attribute) } ||
|
115
|
+
if block then !!block.call(item) else false end
|
116
|
+
end
|
117
|
+
|
118
|
+
FIND_NONE = Proc.new do |item, conditions, block|
|
119
|
+
conditions.none? { |attribute, condition| condition === attr(item, attribute) } &&
|
120
|
+
if block then !!block.call(item) else true end
|
121
|
+
end
|
122
|
+
|
123
|
+
FIND_ONE = Proc.new do |item, conditions, block|
|
124
|
+
conditions.one? { |attribute, condition| condition === attr(item, attribute) } ||
|
125
|
+
if block then !!block.call(item) else false end
|
126
|
+
end
|
127
|
+
|
128
|
+
def key(item)
|
129
|
+
item.send(@key)
|
130
|
+
end
|
131
|
+
|
132
|
+
def attr(item, attribute)
|
133
|
+
item.send(attribute)
|
134
|
+
end
|
135
|
+
|
136
|
+
end
|
137
|
+
|
138
|
+
class HashStore < ObjectStore
|
139
|
+
|
140
|
+
def self.from_hash(hash)
|
141
|
+
self.new(hash[:key] || hash["key"], hash[:items] || hash["items"])
|
142
|
+
end
|
143
|
+
|
144
|
+
def initialize(key=nil, items={})
|
145
|
+
@key, @items = key, items
|
146
|
+
end
|
147
|
+
|
148
|
+
def to_hash
|
149
|
+
{ key: @key, items: @items }
|
150
|
+
end
|
151
|
+
|
152
|
+
private
|
153
|
+
|
154
|
+
def key(item)
|
155
|
+
if @key.nil? then item.hash else item[@key] end
|
156
|
+
end
|
157
|
+
|
158
|
+
def attr(item, attribute)
|
159
|
+
item[attribute]
|
160
|
+
end
|
161
|
+
|
162
|
+
end
|
163
|
+
|
164
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require "json"
|
2
|
+
|
3
|
+
module MemStore
|
4
|
+
|
5
|
+
class HashStore
|
6
|
+
|
7
|
+
def to_json
|
8
|
+
self.to_hash.to_json
|
9
|
+
end
|
10
|
+
|
11
|
+
def to_json_file(file)
|
12
|
+
IO.write file, self.to_json
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.from_json(json)
|
16
|
+
self.from_hash JSON.parse(json)
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.from_json_file(file)
|
20
|
+
self.from_json IO.read(file)
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require "msgpack"
|
2
|
+
|
3
|
+
module MemStore
|
4
|
+
|
5
|
+
class HashStore
|
6
|
+
|
7
|
+
def to_msgpack
|
8
|
+
self.to_hash.to_msgpack
|
9
|
+
end
|
10
|
+
|
11
|
+
def to_msgpack_file(file)
|
12
|
+
IO.write file, self.to_msgpack
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.from_msgpack(msgpack)
|
16
|
+
self.from_hash MessagePack.unpack(msgpack)
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.from_msgpack_file(file)
|
20
|
+
self.from_msgpack IO.read(file)
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require "yaml"
|
2
|
+
|
3
|
+
module MemStore
|
4
|
+
|
5
|
+
class HashStore
|
6
|
+
|
7
|
+
def to_yaml
|
8
|
+
self.to_hash.to_yaml
|
9
|
+
end
|
10
|
+
|
11
|
+
def to_yaml_file(file)
|
12
|
+
IO.write file, self.to_yaml
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.from_yaml(yaml)
|
16
|
+
self.from_hash YAML.load(yaml)
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.from_yaml_file(file)
|
20
|
+
self.from_yaml IO.read(file)
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
data/memstore.gemspec
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require "memstore/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.name = "memstore"
|
8
|
+
gem.version = MemStore::VERSION
|
9
|
+
gem.summary = %q{A simple, in-memory data store.}
|
10
|
+
gem.description = %q{MemStore is a simple in-memory data store that supports adding, retrieving and deleting items as well as complex search queries and easy serialization.}
|
11
|
+
gem.authors = ["Sebastian Klepper"]
|
12
|
+
gem.email = ["sk@sebastianklepper.com"]
|
13
|
+
gem.homepage = "https://github.com/sklppr/memstore"
|
14
|
+
gem.files = `git ls-files`.split($/)
|
15
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
16
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
17
|
+
gem.require_paths = ["lib"]
|
18
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require "minitest/autorun"
|
4
|
+
require "tempfile"
|
5
|
+
require "memstore"
|
6
|
+
require "memstore/json"
|
7
|
+
|
8
|
+
describe MemStore::HashStore do
|
9
|
+
|
10
|
+
before do
|
11
|
+
@key = "id"
|
12
|
+
@mb = MemStore::HashStore.new(@key)
|
13
|
+
10.times { |i| @mb << { "id" => i.to_s } }
|
14
|
+
end
|
15
|
+
|
16
|
+
it "can be converted to and from JSON" do
|
17
|
+
restored = MemStore::HashStore.from_json(@mb.to_json)
|
18
|
+
restored.items.must_equal @mb.items
|
19
|
+
restored.instance_variable_get(:@key).must_equal @key
|
20
|
+
end
|
21
|
+
|
22
|
+
it "can be serialized to and deserialized from a JSON file" do
|
23
|
+
tmp = Tempfile.new("memstore_json")
|
24
|
+
@mb.to_json_file(tmp)
|
25
|
+
restored = MemStore::HashStore.from_json_file(tmp)
|
26
|
+
restored.items.must_equal @mb.items
|
27
|
+
restored.instance_variable_get(:@key).must_equal @key
|
28
|
+
tmp.unlink
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require "minitest/autorun"
|
4
|
+
require "tempfile"
|
5
|
+
require "memstore"
|
6
|
+
require "memstore/msgpack"
|
7
|
+
|
8
|
+
describe MemStore::HashStore do
|
9
|
+
|
10
|
+
before do
|
11
|
+
@key = "id"
|
12
|
+
@mb = MemStore::HashStore.new(@key)
|
13
|
+
10.times { |i| @mb << { "id" => i } }
|
14
|
+
end
|
15
|
+
|
16
|
+
it "can be converted to and from MessagePack" do
|
17
|
+
restored = MemStore::HashStore.from_msgpack(@mb.to_msgpack)
|
18
|
+
restored.items.must_equal @mb.items
|
19
|
+
restored.instance_variable_get(:@key).must_equal @key
|
20
|
+
end
|
21
|
+
|
22
|
+
it "can be serialized to and deserialized from a MessagePack file" do
|
23
|
+
tmp = Tempfile.new("memstore_json")
|
24
|
+
@mb.to_msgpack_file(tmp)
|
25
|
+
restored = MemStore::HashStore.from_msgpack_file(tmp)
|
26
|
+
restored.items.must_equal @mb.items
|
27
|
+
restored.instance_variable_get(:@key).must_equal @key
|
28
|
+
tmp.unlink
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
@@ -0,0 +1,192 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require "minitest/autorun"
|
4
|
+
require "tempfile"
|
5
|
+
require "memstore"
|
6
|
+
|
7
|
+
describe MemStore::ObjectStore do
|
8
|
+
|
9
|
+
it "can be instantiated with items" do
|
10
|
+
h = { a: 1, b: 2, c: 3 }
|
11
|
+
mb = MemStore::ObjectStore.new(nil, h)
|
12
|
+
mb.items.must_equal h
|
13
|
+
end
|
14
|
+
|
15
|
+
it "is the default when instantiating MemStore" do
|
16
|
+
MemStore.new.must_be_instance_of MemStore::ObjectStore
|
17
|
+
end
|
18
|
+
|
19
|
+
it "indexes items by Object#hash by default" do
|
20
|
+
o = Object.new
|
21
|
+
mb = MemStore::ObjectStore.new.insert(o)
|
22
|
+
mb.items[o.hash].must_equal o
|
23
|
+
end
|
24
|
+
|
25
|
+
it "indexes items using a custom key" do
|
26
|
+
o = Struct.new(:id).new(id: "custom key")
|
27
|
+
mb = MemStore::ObjectStore.new(:id).insert(o)
|
28
|
+
mb.items[o.id].must_equal o
|
29
|
+
end
|
30
|
+
|
31
|
+
it "can be serialized and deserialized" do
|
32
|
+
tmp = Tempfile.new("memstore")
|
33
|
+
MemStore::ObjectStore.new.to_file tmp
|
34
|
+
MemStore.from_file(tmp).must_be_instance_of MemStore::ObjectStore
|
35
|
+
tmp.unlink
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
describe MemStore::HashStore do
|
41
|
+
|
42
|
+
it "can be instantiated with items" do
|
43
|
+
h = { a: 1, b: 2, c: 3 }
|
44
|
+
mb = MemStore::HashStore.new(nil, h)
|
45
|
+
mb.items.must_equal h
|
46
|
+
end
|
47
|
+
|
48
|
+
it "indexes items by Object#hash by default" do
|
49
|
+
h = {}
|
50
|
+
mb = MemStore::HashStore.new.insert(h)
|
51
|
+
mb.items[h.hash].must_equal h
|
52
|
+
end
|
53
|
+
|
54
|
+
it "indexes items using a custom key" do
|
55
|
+
h = { id: "custom key" }
|
56
|
+
mb = MemStore::HashStore.new(:id).insert(h)
|
57
|
+
mb.items[h[:id]].must_equal h
|
58
|
+
end
|
59
|
+
|
60
|
+
it "can be serialized and deserialized" do
|
61
|
+
tmp = Tempfile.new("memstore")
|
62
|
+
MemStore::HashStore.new.to_file tmp
|
63
|
+
MemStore.from_file(tmp).must_be_instance_of MemStore::HashStore
|
64
|
+
tmp.unlink
|
65
|
+
end
|
66
|
+
|
67
|
+
it "can be converted to and from a hash" do
|
68
|
+
mb = MemStore::HashStore.new(:id)
|
69
|
+
10.times { |i| mb << { id: i } }
|
70
|
+
restored = MemStore::HashStore.from_hash(mb.to_hash)
|
71
|
+
restored.items.must_equal mb.items
|
72
|
+
restored.instance_variable_get(:@key).must_equal :id
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
|
77
|
+
describe MemStore do
|
78
|
+
|
79
|
+
before do
|
80
|
+
@mb = MemStore.new(:to_i)
|
81
|
+
# Use float as objects and integer as key
|
82
|
+
10.times { |i| @mb << i.to_f }
|
83
|
+
end
|
84
|
+
|
85
|
+
it "returns a single item by itself" do
|
86
|
+
@mb[3].must_equal 3.0
|
87
|
+
end
|
88
|
+
|
89
|
+
it "returns multiple items as an array" do
|
90
|
+
@mb[3, 4, 5, 6].must_equal [3.0, 4.0, 5.0, 6.0]
|
91
|
+
end
|
92
|
+
|
93
|
+
it "returns multiple items using a Range as an array" do
|
94
|
+
@mb[0..9].must_equal @mb.all
|
95
|
+
end
|
96
|
+
|
97
|
+
it "deletes a single item by reference and returns it by itself" do
|
98
|
+
@mb.delete_item(3.0).must_equal 3.0
|
99
|
+
end
|
100
|
+
|
101
|
+
it "deletes multiple items by reference and returns them" do
|
102
|
+
@mb.delete_items(3.0, 4.0, 5.0, 6.0).must_equal [3.0, 4.0, 5.0, 6.0]
|
103
|
+
@mb.all.must_equal [0.0, 1.0, 2.0, 7.0, 8.0, 9.0]
|
104
|
+
end
|
105
|
+
|
106
|
+
it "deletes a single item by key and returns it by itself" do
|
107
|
+
@mb.delete_key(3).must_equal 3.0
|
108
|
+
end
|
109
|
+
|
110
|
+
it "deletes multiple items by key and returns them as an array" do
|
111
|
+
@mb.delete_keys(3, 4, 5, 6).must_equal [3.0, 4.0, 5.0, 6.0]
|
112
|
+
@mb.all.must_equal [0.0, 1.0, 2.0, 7.0, 8.0, 9.0]
|
113
|
+
end
|
114
|
+
|
115
|
+
it "deletes multiple items by key using a Range and returns them as an array" do
|
116
|
+
@mb.delete_keys(3..6).must_equal [3.0, 4.0, 5.0, 6.0]
|
117
|
+
@mb.all.must_equal [0.0, 1.0, 2.0, 7.0, 8.0, 9.0]
|
118
|
+
end
|
119
|
+
|
120
|
+
it "can be serialized and deserialized" do
|
121
|
+
tmp = Tempfile.new("memstore")
|
122
|
+
@mb.to_file tmp
|
123
|
+
MemStore.from_file(tmp).items.must_equal @mb.items
|
124
|
+
tmp.unlink
|
125
|
+
end
|
126
|
+
|
127
|
+
end
|
128
|
+
|
129
|
+
Dummy = Struct.new(:id, :name, :child)
|
130
|
+
|
131
|
+
describe MemStore do
|
132
|
+
|
133
|
+
before do
|
134
|
+
@mb = MemStore.new
|
135
|
+
strings = %w(foo moo boo faa maa baa foa moa boa lao)
|
136
|
+
classes = [String, Array]
|
137
|
+
10.times do |i|
|
138
|
+
@mb << Dummy.new(i, strings[i], classes[i%2].new)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
it "finds all items fulfilling all conditions" do
|
143
|
+
matches = @mb.find_all(id: 3..7, child: String)
|
144
|
+
matches.collect{ |m| m.id }.must_equal [4, 6]
|
145
|
+
end
|
146
|
+
|
147
|
+
it "finds all items fulfilling at least one condition" do
|
148
|
+
matches = @mb.find_any(id: 3..7, child: String)
|
149
|
+
matches.collect{ |m| m.id }.must_equal [0, 2, 3, 4, 5, 6, 7, 8]
|
150
|
+
end
|
151
|
+
|
152
|
+
it "finds all items fulfilling exactly one condition" do
|
153
|
+
matches = @mb.find_one(name: /o/, child: String)
|
154
|
+
matches.collect{ |m| m.id }.must_equal [1, 4, 7, 9]
|
155
|
+
end
|
156
|
+
|
157
|
+
it "finds all items violating at least one condition" do
|
158
|
+
matches = @mb.find_not_all(name: /o/, child: String)
|
159
|
+
matches.collect{ |m| m.id }.must_equal [1, 3, 4, 5, 7, 9]
|
160
|
+
end
|
161
|
+
|
162
|
+
it "finds all items violating all conditions" do
|
163
|
+
matches = @mb.find_none(name: /o/, child: String)
|
164
|
+
matches.collect{ |m| m.id }.must_equal [3, 5]
|
165
|
+
end
|
166
|
+
|
167
|
+
it "finds the first item fulfilling all conditions" do
|
168
|
+
match = @mb.first_all(id: 3..7, child: String)
|
169
|
+
match.id.must_equal 4
|
170
|
+
end
|
171
|
+
|
172
|
+
it "finds the first item fulfilling at least one condition" do
|
173
|
+
match = @mb.first_any(id: 3..7, child: String)
|
174
|
+
match.id.must_equal 0
|
175
|
+
end
|
176
|
+
|
177
|
+
it "finds the first item fulfilling exactly one condition" do
|
178
|
+
match = @mb.first_one(name: /o/, child: String)
|
179
|
+
match.id.must_equal 1
|
180
|
+
end
|
181
|
+
|
182
|
+
it "finds the first item violating at least one condition" do
|
183
|
+
match = @mb.first_not_all(name: /o/, child: String)
|
184
|
+
match.id.must_equal 1
|
185
|
+
end
|
186
|
+
|
187
|
+
it "finds the first item violating all conditions" do
|
188
|
+
match = @mb.first_none(name: /o/, child: String)
|
189
|
+
match.id.must_equal 3
|
190
|
+
end
|
191
|
+
|
192
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require "minitest/autorun"
|
4
|
+
require "tempfile"
|
5
|
+
require "memstore"
|
6
|
+
require "memstore/yaml"
|
7
|
+
|
8
|
+
describe MemStore::HashStore do
|
9
|
+
|
10
|
+
before do
|
11
|
+
@key = :id
|
12
|
+
@mb = MemStore::HashStore.new(@key)
|
13
|
+
10.times { |i| @mb << { id: i } }
|
14
|
+
end
|
15
|
+
|
16
|
+
it "can be converted to and from YAML" do
|
17
|
+
restored = MemStore::HashStore.from_yaml(@mb.to_yaml)
|
18
|
+
restored.items.must_equal @mb.items
|
19
|
+
restored.instance_variable_get(:@key).must_equal @key
|
20
|
+
end
|
21
|
+
|
22
|
+
it "can be serialized to and deserialized from a YAML file" do
|
23
|
+
tmp = Tempfile.new("memstore_yaml")
|
24
|
+
@mb.to_yaml_file(tmp)
|
25
|
+
restored = MemStore::HashStore.from_yaml_file(tmp)
|
26
|
+
restored.items.must_equal @mb.items
|
27
|
+
restored.instance_variable_get(:@key).must_equal @key
|
28
|
+
tmp.unlink
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
metadata
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: memstore
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Sebastian Klepper
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-02-19 00:00:00.000000000 Z
|
13
|
+
dependencies: []
|
14
|
+
description: MemStore is a simple in-memory data store that supports adding, retrieving
|
15
|
+
and deleting items as well as complex search queries and easy serialization.
|
16
|
+
email:
|
17
|
+
- sk@sebastianklepper.com
|
18
|
+
executables: []
|
19
|
+
extensions: []
|
20
|
+
extra_rdoc_files: []
|
21
|
+
files:
|
22
|
+
- .gitignore
|
23
|
+
- Gemfile
|
24
|
+
- LICENSE.txt
|
25
|
+
- README.md
|
26
|
+
- Rakefile
|
27
|
+
- lib/memstore.rb
|
28
|
+
- lib/memstore/json.rb
|
29
|
+
- lib/memstore/msgpack.rb
|
30
|
+
- lib/memstore/version.rb
|
31
|
+
- lib/memstore/yaml.rb
|
32
|
+
- 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
|
37
|
+
homepage: https://github.com/sklppr/memstore
|
38
|
+
licenses: []
|
39
|
+
post_install_message:
|
40
|
+
rdoc_options: []
|
41
|
+
require_paths:
|
42
|
+
- lib
|
43
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
44
|
+
none: false
|
45
|
+
requirements:
|
46
|
+
- - ! '>='
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: '0'
|
49
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
requirements: []
|
56
|
+
rubyforge_project:
|
57
|
+
rubygems_version: 1.8.25
|
58
|
+
signing_key:
|
59
|
+
specification_version: 3
|
60
|
+
summary: A simple, in-memory data store.
|
61
|
+
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
|