memstore 1.2.1 → 2.0.1

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