memstore 1.2.1 → 2.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: e09df24ba555d687f405c140c6ccbacaf4b42963
4
+ data.tar.gz: 462f77bf9be2d36d7cd98f770c59b65149fdd5e8
5
+ SHA512:
6
+ metadata.gz: 827ae9142869655c018017fe3ca335333561dc9504ab0ef46b5e0dae02b21b3b8f9c83bdad00090a6e9fc2c1ca9fc4fa289795cc1cdd5465a12680b07d037efb
7
+ data.tar.gz: 3c92d873c50ddbb71f8ec8a67190edc65f003a67221526ceae430ee6446fb3a3bc6a3714ee2d5b76c07448265af1862e99668eb638b8fabd3990f719d7f38ec5
data/.gitignore CHANGED
@@ -15,3 +15,4 @@ spec/reports
15
15
  test/tmp
16
16
  test/version_tmp
17
17
  tmp
18
+ .ruby-version
data/.rdoc_options ADDED
@@ -0,0 +1,19 @@
1
+ --- !ruby/object:RDoc::Options
2
+ encoding: UTF-8
3
+ charset: UTF-8
4
+ static_path: []
5
+ rdoc_include:
6
+ - .
7
+ exclude:
8
+ - spec
9
+ - Gemfile
10
+ - Gemfile.lock
11
+ - Rakefile
12
+ - created.rid
13
+ main_page: README.md
14
+ markup: tomdoc
15
+ hyperlink_all: false
16
+ line_numbers: false
17
+ show_hash: false
18
+ tab_width: 2
19
+ visibility: :protected
data/README.md CHANGED
@@ -2,265 +2,134 @@
2
2
 
3
3
  *A simple in-memory data store.*
4
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.
5
+ MemStore is a simple in-memory data store that supports complex search queries.
6
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.
7
+ It’s not in any way supposed to be a database. However, it can be used instead of database in small applications or prototypes.
8
8
 
9
- ## Installation
9
+ **Important: Ruby 2.1 is required.**
10
10
 
11
- Add this line to your application's Gemfile:
11
+ ## Basics
12
12
 
13
- ```ruby
14
- gem "memstore"
15
- ```
16
-
17
- And then execute:
18
-
19
- ```sh
20
- $ bundle
21
- ```
22
-
23
- Or install it yourself as:
24
-
25
- ```sh
26
- $ gem install memstore
27
- ```
28
-
29
- ## Usage
30
-
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)
36
- - [Search Queries](#search-queries)
37
- - [Serialization](#serialization)
38
- - [Binary](#binary)
39
- - [Hash](#hash)
40
- - [YAML](#yaml)
41
- - [JSON](#json)
42
- - [MessagePack](#messagepack)
43
- - [Concurrent Access](#concurrent-access)
44
-
45
- ### Basics
46
-
47
- Creating a data store is utterly simple:
13
+ Creating a data store is straightforward:
48
14
 
49
15
  ```ruby
50
16
  store = MemStore.new
51
- ```
52
-
53
- By default, objects are indexed using `Object#hash`.
54
-
55
- If a different property should be used, it can be specified like this:
56
-
57
- ```ruby
58
- store = MemStore.new(:id)
59
- ```
60
-
61
- The property needs to be truly unique for all objects since it’s used as a hash key internally.
62
-
63
- An items collection can also be provided on creation:
64
-
65
- ```ruby
66
- store = MemStore.new(nil, { ... }) # to use Object.hash as key
67
- store = MemStore.new(:id, { ... }) # to use custom key
68
- ```
69
-
70
- The collection must be a hash that correctly maps the used key to each item.
71
-
72
- #### Objects vs. Hashes
73
-
74
- MemStore comes in two flavors: `ObjectStore` and `HashStore`.
75
-
76
- They’re basically the same, but `ObjectStore` accesses items through `item.attribute` while `HashStore` accesses items through `item[attribute]`.
77
-
78
- `ObjectStore` is the default variant:
79
-
80
- ```ruby
81
- store = MemStore.new
82
- # is equal to
83
- store = MemStore::ObjectStore.new
84
- ```
85
-
86
- `HashStore` needs to be created explicitly:
87
-
88
- ```ruby
89
- store = MemStore::HashStore.new
90
- ```
91
-
92
- If no key attribute is specified, `HashStore` will also use `Object#hash`.
93
-
94
- #### Adding Items
95
-
96
- `items` provides direct access to the internal items hash.
97
-
98
- ```ruby
99
- store.items
100
- # => {}
101
- store.items = { 1 => a, 2 => b, 3 => c }
102
- # => { 1 => a, 2 => b, 3 => c }
103
- ```
104
-
105
- `insert` adds one or multiple items and returns the data store itself:
106
-
107
- ```ruby
108
- store.insert(a, b, c)
109
17
  # => store
110
18
  ```
111
19
 
112
- Since it returns the data store, items can be added right after instantiation:
20
+ Adding items is equally simple. Add a single item using the shovel operator `<<` or multiple items using `add`:
113
21
 
114
22
  ```ruby
115
- store = MemStore.new.insert(a, b, c)
23
+ store << a
116
24
  # => store
117
- ```
118
-
119
- MemStore also supports the shovel operator `<<` for adding items.
120
- Only one item can be added at a time but it’s chainable:
121
-
122
- ```ruby
123
- store << a << b << c
25
+ store.add(a, b, c)
124
26
  # => store
125
27
  ```
126
28
 
127
- #### Retrieving Items
128
-
129
- `length` (or `size`) returns the current number of items:
29
+ To make things easier, MemStore’s constructor takes a collection of items that will be added right away:
130
30
 
131
31
  ```ruby
132
- store.length
133
- # => 3
32
+ store = MemStore.new(items: [a, b, c])
33
+ # => store
134
34
  ```
135
35
 
136
- The bracket operator `[]` is used to look up items by their key.
137
- If a single key is given, a single item will be returned.
138
- If multiple keys are given, an array of items will be returned with `nil` when there is no item for a key.
36
+ You can access single items by their key (see [Customization](#customization)) using the bracket operator `[]` or multiple items using `get`:
139
37
 
140
38
  ```ruby
141
39
  store[1]
142
40
  # => a
143
- store[1, 2, 3]
41
+ store.get(1, 2, 3)
144
42
  # => [a, b, c]
145
43
  ```
146
44
 
147
- Ranges are also supported and can even be combined with single keys:
45
+ You can also get all items at once using `all` and a hash of all items with their keys using `items`:
148
46
 
149
47
  ```ruby
150
- store[1..3]
48
+ store.all
151
49
  # => [a, b, c]
152
- store[1..3, 6]
153
- # => [a, b, c, f]
50
+ store.items
51
+ # => { 1 => a, 2 => b, 3 => c }
154
52
  ```
155
53
 
156
- #### Deleting Items
54
+ ## Queries
157
55
 
158
- `delete_items` (or `delete_item`) deletes items by reference and returns them.
159
- This is considered the default use case and therefore also available as `delete`.
56
+ MemStore provides methods to find, count and delete items using complex queries:
160
57
 
161
- If one item is given, it is deleted and returned.
162
- If multiple items are given, they are deleted and returned as an array.
58
+ - `find_*` returns all items matching the query
59
+ - `first_*` returns the first item matching the query
60
+ - `count_*` returns the number of items matching the query
61
+ - `delete_*` deletes and returns all items matching the query
163
62
 
164
- ```ruby
165
- store.delete_item(a)
166
- # => a
167
- store.delete_items(b, c, d)
168
- # => [b, c, d]
169
- store.delete(e, f, g)
170
- # => [e, f, g]
171
- ```
172
-
173
- This is considered the default use case and therefore also available as `delete`.
174
-
175
- `delete_keys` (or `delete_key`) deletes items by key and returns them.
176
- Again, one or multiple items can be deleted at a time and even ranges are accepted.
177
-
178
- ```ruby
179
- store.delete_key(1)
180
- # => a
181
- store.delete_keys(2, 3, 4)
182
- # => [b, c, d]
183
- store.delete_keys(5..7, 9)
184
- # => [e, f, g, i]
185
- ```
63
+ These methods have one of the following suffixes:
186
64
 
187
- ### Search Queries
65
+ - `*_all` matches items *fulfilling all* conditions
66
+ - `*_any` matches items *fulfilling at least one* condition
67
+ - `*_one` matches items *fulfilling exactly one* condition
68
+ - `*_not_all` matches items *violating at least one* condition
69
+ - `*_none` matches items *violating all* conditions
188
70
 
189
- The following methods are available to query the data store:
190
-
191
- - `find_all` (alias `find`)
192
- - `find_any`
193
- - `find_one`
194
- - `find_not_all`
195
- - `find_none`
196
- - `first_all` (alias `first`)
197
- - `first_any`
198
- - `first_one`
199
- - `first_not_all`
200
- - `first_none`
201
- - `count_all` (alias `count`)
202
- - `count_any`
203
- - `count_one`
204
- - `count_not_all`
205
- - `count_none`
206
-
207
- The first part indicates what is returned:
208
-
209
- - `find_*` returns all matches.
210
- - `first_*` returns the first match.
211
- - `count_*` returns the number of matches.
71
+ In other words:
212
72
 
213
- The second part indicates how conditions are evaluated:
73
+ - `all` means `condition && condition && ...`
74
+ - `any` means `condition || condition || ...`
75
+ - `one` means `condition ^ condition ^ ...`
76
+ - `not all` means `!(condition && condition && ...)` or `!condition || !condition || ...`
77
+ - `none` means `!(condition || condition || ...)` or `!condition && !condition && ...`
214
78
 
215
- - `*_all` matches items *fulfilling all* conditions.
216
- - `*_any` matches items *fulfilling at least one* condition.
217
- - `*_one` matches items *fulfilling exactly one* condition.
218
- - `*_not_all` matches items *violating at least one* condition.
219
- - `*_none` matches items *violating all* conditions.
79
+ For convenience, there are aliases for the `*_all` variants:
220
80
 
221
- In other words:
81
+ - `find` is an alias of `find_all`
82
+ - `first` is an alias of `first_all`
83
+ - `count` is an alias of `count_all`
84
+ - `delete` is an alias of `delete_all`
222
85
 
223
- - `all` means `condition && condition && ...`.
224
- - `any` means `condition || condition || ...`.
225
- - `one` means `condition ^ condition ^ ...`.
226
- - `not all` means `!(condition && condition && ...)` or `!condition || !condition || ...`.
227
- - `none` means `!(condition || condition || ...)` or `!condition && !condition && ...`.
86
+ All methods take a hash of conditions and/or a block.
228
87
 
229
- All variants take a `conditions` hash and an optional block.
88
+ The hash is expected to map attributes (see [Customization](#customization)) to conditions.
89
+ Conditions are evaluated using the case equality operator: `condition === item`
230
90
 
231
- The hash maps attributes names to conditions that should be tested.
232
- Conditions are evaluated using the `===` operator and can be virtually anything:
91
+ This means conditions can be virtually anything:
233
92
 
234
93
  ```ruby
235
- store.find(name: "Fred", age: 25)
236
- store.find(name: /red/i, age: 10..30)
94
+ store.find(name: "John", age: 42)
95
+ # is equivalent to item.name == "John" && item.age == 42
96
+ store.find(name: /^Jo/, age: 23..42)
97
+ # is equivalent to /^Jo/ =~ item.name && (23..42).include?(item.age)
237
98
  store.find(child: MyClass)
99
+ # is equivalent to item.child.kind_of?(MyClass)
238
100
  store.find(child: -> child { child.valid? })
101
+ # is equivalent to proc.call(item.child)
239
102
  ```
240
103
 
241
- Additional types can be used in conditions by supporting the `===` operator. For example:
104
+ You can enable additional types of conditions simply by implementing `===`.
105
+ For example, MemStore also supports arrays using an internal refinement:
242
106
 
243
107
  ```ruby
244
- class Array
245
- def ===(obj)
246
- self.include?(obj)
247
- end
248
- end
249
-
250
108
  store.find(age: [23, 25, 27])
109
+ # is equivalent to [23, 25, 27].include?(item.age)
110
+ ```
111
+
112
+ The implementation looks like this:
113
+
114
+ ```ruby
115
+ refine Array do
116
+ def ===(obj)
117
+ include?(obj)
118
+ end
119
+ end
251
120
  ```
252
121
 
253
- The block is invoked with the item *after* the conditions are evaluated. It should return a boolean value:
122
+ The block is invoked with the item *after* the conditions are evaluated.
254
123
 
255
124
  ```ruby
256
125
  store.find(age: 25) { |item| item.age - item.child.age > 20 }
257
- # is evaluated as (item.age == 25) && (item.age - item.child.age > 20)
126
+ # is equivalent to item.age == 25 && item.age - item.child.age > 20
258
127
  ```
259
128
 
260
- In addition to the evaluation logic, the arrays returned by all variants of `find_*` can be merged:
129
+ In addition to the query logic, you can merge the arrays returned by `find_*`:
261
130
 
262
131
  ```ruby
263
- store.find(...) | store.find(...) | store.find(...)
132
+ store.find_all(...) | store.find_any(...) | store.find_none(...)
264
133
  ```
265
134
 
266
135
  Note that the pipe operator `|` already eliminates duplicates:
@@ -270,173 +139,128 @@ Note that the pipe operator `|` already eliminates duplicates:
270
139
  # => [a, b, c, d, e]
271
140
  ```
272
141
 
273
- ### Serialization
142
+ ## Customization
274
143
 
275
- MemStore support various ways of de-/serializing the data store.
144
+ ### Default Behavior
276
145
 
277
- - `ObjectStore` supports binary format.
278
- - `HashStore` supports binary format, hash, [YAML](http://yaml.org/), [JSON](http://www.json.org/) and [MessagePack](http://msgpack.org/).
279
-
280
- **Important:** When file IO or deserialization fails, all variants of `from_*` return `nil`.
281
-
282
- The following style ensures that there will be a (correctly configured) data store:
146
+ By default, MemStore indexes items using `Object#hash`:
283
147
 
284
148
  ```ruby
285
- store = MemStore.from_file(file) || MemStore::HashStore(key, items)
149
+ store = MemStore.new
150
+ store << item
151
+ # calls item.hash to retrieve key
152
+ store[item.hash]
153
+ # => item
286
154
  ```
287
155
 
288
- #### Binary
289
-
290
- Both `ObjectStore` and `HashStore` can easily be serialized to and from binary format:
156
+ When you use `find_*`, `first_*`, `count_*` or `delete_*`, MemStore calls attributes as methods on your items using `Object#send`:
291
157
 
292
158
  ```ruby
293
- store.to_binary
294
- # => binary string
295
- store.to_file(file)
296
- # => number of bytes written
297
- MemStore.from_binary(binary)
298
- # => instance of ObjectStore or HashStore or nil
299
- MemStore.from_file(file)
300
- # => instance of ObjectStore or HashStore or nil
159
+ store = MemStore.new
160
+ store << item
161
+ store.find(age: 42, name: "John")
162
+ # calls item.age and item.name to retrieve attributes
301
163
  ```
302
164
 
303
- #### Hash
304
-
305
- `HashStore` can be converted to and from a hash:
165
+ This means that it doesn’t make a difference whether you use strings or symbols in the conditions hash:
306
166
 
307
167
  ```ruby
308
- store.to_hash
309
- # => { key: ..., items: { ... } }
310
- MemStore::HashStore.from_hash(hash)
311
- # => instance of HashStore or nil
168
+ store.find("age" => 42, "name" => "John")
169
+ # calls item.age and item.name to retrieve attributes
312
170
  ```
313
171
 
314
- #### YAML
172
+ *Note that using Strings will result in a performance penalty because `Object#send` expects Symbols.*
173
+
174
+ ### Custom Key
315
175
 
316
- `memstore/yaml` enables serialization of `HashStore` to and from [YAML](http://yaml.org/):
176
+ You’ll probably want MemStore to use a specific attribute to index items.
177
+ This is possible using the `key` parameter when creating a data store:
317
178
 
318
179
  ```ruby
319
- require "memstore/yaml" # requires "yaml"
320
-
321
- store.to_yaml
322
- # => YAML string
323
- store.to_yaml_file(file)
324
- # => number of bytes written
325
- MemStore::HashStore.from_yaml(yaml)
326
- # => instance of HashStore or nil
327
- MemStore::HashStore.from_yaml_file(file)
328
- # => instance of HashStore or nil
180
+ store = MemStore.new(key: :id)
181
+ store << item
182
+ # calls item.id to retrieve key
183
+ store[item.id]
184
+ # => item
329
185
  ```
330
186
 
331
- De/serialization is seamless since YAML can handle symbols and non-string keys (i.e. Psych converts them correctly).
187
+ Whatever you provide as `key` will be treated as an attribute.
188
+ So, by default, the according method will be called on your item.
332
189
 
333
- #### JSON
190
+ ### Custom Access Method
334
191
 
335
- `memstore/json` enables serialization of `HashStore` to and from [JSON](http://www.json.org/):
192
+ If you want to change how attributes are accessed, you can use the `access` parameter when creating a data store:
336
193
 
337
194
  ```ruby
338
- require "memstore/json" # requires "json"
339
-
340
- store.to_json
341
- # => JSON string
342
- store.to_json_file(file)
343
- # => number of bytes written
344
- MemStore::HashStore.from_json(json)
345
- # => instance of HashStore or nil
346
- MemStore::HashStore.from_json_file(file)
347
- # => instance of HashStore or nil
195
+ store = MemStore.new(key: :id, access: :[])
196
+ # now you can store hashes, e.g. { id: 5, age: 42, name: "John" }
197
+ store << item
198
+ # calls item[:id] to retrieve key
199
+ store.find(age: 42, name: "John")
200
+ # calls item[:age] and item[:name] to retrieve attributes
348
201
  ```
349
202
 
350
- **Important:** Symbols will be converted to strings and JSON only allows string keys.
203
+ If you provide a Symbol or String, it will be treated as a method name.
204
+ *Note that providing a String will result in a performance penalty because `Object#send` expects Symbols.*
351
205
 
352
- ```ruby
353
- store = MemStore::HashStore.new(:id)
354
- store << { id: 1 }
355
- store.to_hash
356
- # => { :key => :id, :items => { 1 => { :id => 1 } } }
357
- store = MemStore::HashStore.from_json(store.to_json)
358
- store.to_hash
359
- # => { :key => "id", :items => { "1" => { "id" => 1 } } }
360
- ```
361
-
362
- The following style ensures consistent access before and after serialization:
206
+ To access an attribute, MemStore will call the according method on your item and pass the requested attribute to it.
207
+ This means `key` and attributes in the conditions hash must be whatever your method expects:
363
208
 
364
209
  ```ruby
365
- store = MemStore::HashStore.new("id")
366
- store << { "id" => "1" }
367
- store["1"]
368
- # => { "id" => "1" }
210
+ # assuming that items have a method `get` that expects a String:
211
+ store = MemStore.new(key: "id", access: :get)
212
+ store << item
213
+ # calls item.get("id") to retrieve key
214
+ store.find("age" => 42, "name" => "John")
215
+ # calls item.get("age") and item.get("name") to retrieve attributes
369
216
  ```
370
217
 
371
- #### MessagePack
218
+ ### Advanced Customization
372
219
 
373
- `memstore/msgpack` enables serialization of `HashStore` to and from [MessagePack](http://msgpack.org/):
220
+ If you want to do something special to obtain a key, you can provide a Proc or Method.
221
+ It will be passed the item for which MemStore needs a key and is expected to return a truly unique identifier for that item:
374
222
 
375
223
  ```ruby
376
- require "memstore/msgpack" # requires "msgpack"
377
-
378
- store.to_msgpack
379
- # => MessagePack binary format
380
- store.to_msgpack_file(file)
381
- # => number of bytes written
382
- MemStore::HashStore.from_msgpack(msgpack)
383
- # => instance of HashStore or nil
384
- MemStore::HashStore.from_msgpack_file(file)
385
- # => instance of HashStore or nil
386
- ```
387
-
388
- **Important:** Symbols will be converted to strings but non-string keys are allowed.
224
+ def special_hash(item)
225
+ # ...
226
+ end
389
227
 
390
- ```ruby
391
- store = MemStore::HashStore.new(:id)
392
- store << { id: 1 }
393
- store.to_hash
394
- # => { :key => :id, :items => { 1 => { :id => 1 } } }
395
- store = MemStore::HashStore.from_msgpack(store.to_msgpack)
396
- store.to_hash
397
- # => { :key => "id", :items => { 1 => { "id" => 1 } } }
228
+ # lambda:
229
+ store = MemStore.new(key: -> item { special_hash(item) })
230
+ # Proc:
231
+ store = MemStore.new(key: Proc.new { |item| special_hash(item) })
232
+ # Method:
233
+ store = MemStore.new(key: method(:special_hash))
398
234
  ```
399
235
 
400
- The following style ensures consistent access before and after serialization:
236
+ Note that this is also a way to circumvent the access method for attributes.
237
+ For example, you might want to use one method `get` to access all attributes but a different method `id` should be used for indexing:
401
238
 
402
239
  ```ruby
403
- store = MemStore::HashStore.new("id")
404
- store << { "id" => 1 }
405
- store[1]
406
- # => { "id" => 1 }
240
+ # this way, item.get(:id) would be used:
241
+ store = MemStore.new(access: :get, key: :id)
242
+ # circumvent the access method like this:
243
+ store = MemStore.new(access: :get, key: -> item { item.id })
244
+ # or even shorter:
245
+ store = MemStore.new(access: :get, key: Proc.new(&:id))
246
+ store << item
247
+ # calls item.id to retrieve key
248
+ store.find(age: 42, name: "John")
249
+ # calls item.get(:age) and item.get(:name) to retrieve attributes
407
250
  ```
408
251
 
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.
252
+ Likewise, you can provide a Proc or Method to be called when accessing attributes.
253
+ It will be passed both the item in question and the attribute to be retrieved and is expected to return the appropriate value:
414
254
 
415
255
  ```ruby
416
- MemStore.with_file(file, key, items) do |store|
417
- # create, read, update, delete
256
+ def special_accessor(item, attribute)
257
+ # ...
418
258
  end
419
- ```
420
-
421
- `MemStore.with_file` is a shortcut to `MemStore::ObjectStore.with_file`.
422
259
 
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
-
430
- ## Contributing
431
-
432
- 1. Fork it on [GitHub](https://github.com/sklppr/memstore).
433
- 2. Create a feature branch containing your changes:
434
-
435
- ```sh
436
- $ git checkout -b feature/my-new-feature
437
- # code, code, code
438
- $ git commit -am "Add some feature"
439
- $ git push origin feature/my-new-feature
440
- ```
441
-
442
- 3. Create a Pull Request on [GitHub](https://github.com/sklppr/memstore).
260
+ # lambda:
261
+ store = MemStore.new(access: -> item, attribute { special_accessor(item, attribute) })
262
+ # Proc:
263
+ store = MemStore.new(access: Proc.new { |item| special_accessor(item, attribute) })
264
+ # Method:
265
+ store = MemStore.new(access: method(:special_accessor))
266
+ ```