redis-persistence 0.0.3 → 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- data/README.markdown +8 -6
- data/examples/article.rb +42 -22
- data/examples/benchmark.rb +75 -3
- data/lib/redis/persistence/version.rb +1 -1
- data/lib/redis/persistence.rb +98 -37
- data/test/models.rb +5 -1
- data/test/redis_persistence_test.rb +86 -24
- metadata +22 -21
data/README.markdown
CHANGED
@@ -2,7 +2,7 @@ Redis Persistence
|
|
2
2
|
=================
|
3
3
|
|
4
4
|
`Redis::Persistence` is a lightweight object persistence framework,
|
5
|
-
fully compatible with
|
5
|
+
fully compatible with _ActiveModel_, based on [_Redis_](http://redis.io),
|
6
6
|
easily used standalone or within Rails applications.
|
7
7
|
|
8
8
|
Installation
|
@@ -20,8 +20,10 @@ Features:
|
|
20
20
|
* Defining default values for properties
|
21
21
|
* Casting properties as built-in or custom classes
|
22
22
|
* Convenient "dot access" to properties (<tt>article.views.today</tt>)
|
23
|
-
* Support for "collections" of embedded objects (eg. article
|
23
|
+
* Support for "collections" of embedded objects (eg. article » comments)
|
24
24
|
* Automatic conversion of UTC-formatted strings to Time objects
|
25
|
+
* Small: 1 file, ~ 200 lines of code
|
26
|
+
* Fast: ~2000 saves/sec, ~6000 finds/sec
|
25
27
|
|
26
28
|
Basic example
|
27
29
|
-------------
|
@@ -47,17 +49,17 @@ Basic example
|
|
47
49
|
# => <Article: {"id"=>1, "title"=>"The Thing", ...}>
|
48
50
|
|
49
51
|
article.title
|
50
|
-
# =>
|
52
|
+
# => The Thing!
|
51
53
|
|
52
|
-
article.created.
|
53
|
-
# =>
|
54
|
+
article.created.year
|
55
|
+
# => 2011
|
54
56
|
|
55
57
|
article.title = 'The Cabin'
|
56
58
|
article.save
|
57
59
|
# => <Article: {"id"=>1, "title"=>"The Cabin", ...}>
|
58
60
|
```
|
59
61
|
|
60
|
-
See the [`examples/article.rb`](
|
62
|
+
See the [`examples/article.rb`](http://github.com/Ataxo/redis-persistence/blob/master/examples/article.rb) for full example.
|
61
63
|
|
62
64
|
-----
|
63
65
|
|
data/examples/article.rb
CHANGED
@@ -8,41 +8,57 @@ Redis::Persistence.config.redis.flushdb
|
|
8
8
|
# => #<Redis client v2.2.2 connected to redis://127.0.0.1:6379/0 (Redis v2.4.1)>
|
9
9
|
|
10
10
|
class Comment
|
11
|
-
def initialize(params); @attributes = HashWithIndifferentAccess.new(params);
|
12
|
-
def method_missing(method_name, *arguments); @attributes[method_name];
|
13
|
-
def as_json(*); @attributes;
|
11
|
+
def initialize(params); @attributes = HashWithIndifferentAccess.new(params); end
|
12
|
+
def method_missing(method_name, *arguments); @attributes[method_name]; end
|
13
|
+
def as_json(*); @attributes; end
|
14
|
+
end
|
15
|
+
|
16
|
+
class Body
|
17
|
+
def initialize(params); @body = HashWithIndifferentAccess.new(params)[:body]; end
|
18
|
+
def words
|
19
|
+
@body.split(/\W/).size
|
20
|
+
end
|
21
|
+
def inspect
|
22
|
+
"“#{@body}”"
|
23
|
+
end
|
14
24
|
end
|
15
25
|
|
16
26
|
class Article
|
17
27
|
include Redis::Persistence
|
18
28
|
|
19
29
|
property :title
|
20
|
-
property :body
|
21
|
-
property :author, :default => '(Unknown)'
|
22
30
|
property :created
|
31
|
+
property :author, default: '(Unknown)'
|
32
|
+
property :body, class: Body
|
23
33
|
|
24
|
-
property :comments, :
|
34
|
+
property :comments, default: [], class: [Comment], family: 'comments'
|
25
35
|
end
|
26
36
|
|
27
|
-
article = Article.new :
|
28
|
-
:
|
29
|
-
:
|
30
|
-
:
|
31
|
-
# => #<Article: {"id"=>1, "title"=>"
|
37
|
+
article = Article.new title: 'I Work For Banks Now!',
|
38
|
+
author: 'Malcom Gladwell',
|
39
|
+
body: 'Imagine that I asked you ...',
|
40
|
+
created: Time.now.utc
|
41
|
+
# => #<Article: {"id"=>1, "title"=>"I Work For Banks Now!", ...>
|
32
42
|
|
33
43
|
p article.save
|
34
|
-
# => #<Article: {"id"=>1, "title"=>"
|
44
|
+
# => #<Article: {"id"=>1, "title"=>"I Work For Banks Now!", ...>
|
35
45
|
|
36
46
|
p article = Article.find(1)
|
37
|
-
# => #<Article: {"id"=>1, "title"=>"
|
47
|
+
# => #<Article: {"id"=>1, "title"=>"I Work For Banks Now!", ...>
|
38
48
|
|
39
49
|
p article.title
|
40
|
-
# => "
|
50
|
+
# => "I Work For Banks Now!"
|
41
51
|
|
42
52
|
p article.created.year
|
43
53
|
# => 2011
|
44
54
|
|
45
|
-
article
|
55
|
+
p article.body
|
56
|
+
# => "“Imagine that I asked you ...”"
|
57
|
+
|
58
|
+
p article.body.words
|
59
|
+
# => 5
|
60
|
+
|
61
|
+
article = Article.new title: 'In the Beginning Was the Command Line'
|
46
62
|
p article.save
|
47
63
|
# => #<Article: {"id"=>2, "title"=>"In the Beginning Was the Command Line", ... "author"=>"(Unknown)"}>
|
48
64
|
|
@@ -52,14 +68,14 @@ p article = Article.find(2)
|
|
52
68
|
p article.author
|
53
69
|
# => "(Unknown)"
|
54
70
|
|
55
|
-
article = Article.
|
71
|
+
article = Article.new title: 'OMG BLOG!'
|
56
72
|
|
57
|
-
|
73
|
+
article.comments
|
58
74
|
# => []
|
59
75
|
|
60
|
-
article.comments << {:
|
76
|
+
article.comments << {nick: '4chan', body: 'WHY U NO QUIT?'}
|
61
77
|
|
62
|
-
article.comments << Comment.new(:
|
78
|
+
article.comments << Comment.new(nick: 'h4x0r', body: 'WHY U NO USE BBS?')
|
63
79
|
|
64
80
|
p article.comments.size
|
65
81
|
# => 2
|
@@ -68,10 +84,14 @@ p article.save(families: 'comments')
|
|
68
84
|
# => <Article: {"id"=>3, ... "comments"=>[{:nick=>"4chan", :body=>"WHY U NO QUIT?"}]}>
|
69
85
|
|
70
86
|
article = Article.find(3)
|
71
|
-
|
72
|
-
|
87
|
+
begin
|
88
|
+
article.comments
|
89
|
+
rescue Exception => e
|
90
|
+
p e
|
91
|
+
end
|
92
|
+
# => Redis::Persistence::FamilyNotLoaded ...
|
73
93
|
|
74
|
-
article = Article.find(3, :
|
94
|
+
article = Article.find(3, families: 'comments')
|
75
95
|
p article.comments
|
76
96
|
# => [#<Comment @attributes={"nick"=>"4chan", "body"=>"WHY U NO QUIT?"}>, ...]
|
77
97
|
|
data/examples/benchmark.rb
CHANGED
@@ -1,15 +1,31 @@
|
|
1
1
|
$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
|
2
2
|
|
3
|
+
require 'active_record'
|
4
|
+
require 'sqlite3'
|
5
|
+
|
3
6
|
require 'benchmark'
|
4
7
|
require 'redis/persistence'
|
5
8
|
require 'active_support/core_ext/hash/indifferent_access'
|
6
9
|
|
7
10
|
content = DATA.read
|
8
|
-
COUNT = ENV['COUNT'] || 100_000
|
11
|
+
COUNT = (ENV['COUNT'] || 100_000).to_i
|
9
12
|
|
10
13
|
Redis::Persistence.config.redis = Redis.new :db => 14
|
11
14
|
Redis::Persistence.config.redis.flushdb
|
12
15
|
|
16
|
+
DATABASE_FILE = '/tmp/tire_benchmark_articles.sqlite3'
|
17
|
+
File.delete DATABASE_FILE rescue nil
|
18
|
+
# ActiveRecord::Base.establish_connection( :adapter => 'sqlite3', :database => ":memory:" )
|
19
|
+
ActiveRecord::Base.establish_connection( :adapter => 'sqlite3', :database => DATABASE_FILE )
|
20
|
+
ActiveRecord::Migration.verbose = false
|
21
|
+
ActiveRecord::Schema.define(:version => 1) do
|
22
|
+
create_table :sql_articles do |t|
|
23
|
+
t.string :title
|
24
|
+
t.text :content
|
25
|
+
t.timestamps
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
13
29
|
class Article
|
14
30
|
include Redis::Persistence
|
15
31
|
|
@@ -18,6 +34,52 @@ class Article
|
|
18
34
|
property :created, :family => 'extra'
|
19
35
|
end
|
20
36
|
|
37
|
+
class SQLArticle < ActiveRecord::Base
|
38
|
+
end
|
39
|
+
|
40
|
+
puts "Beginning the benchmark script (SQLite and Redis)...", "", '='*80
|
41
|
+
sleep 5
|
42
|
+
|
43
|
+
puts "Saving #{COUNT} records into a SQLite database..."
|
44
|
+
|
45
|
+
elapsed = Benchmark.realtime do
|
46
|
+
(1..COUNT).map do |i|
|
47
|
+
SQLArticle.create title: "Document #{i}", content: content
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
puts "Duration: #{elapsed} seconds, rate: #{COUNT.to_f/elapsed} docs/sec",
|
52
|
+
'-'*80
|
53
|
+
|
54
|
+
puts "Finding all records from SQLite..."
|
55
|
+
|
56
|
+
elapsed = Benchmark.realtime do
|
57
|
+
SQLArticle.all
|
58
|
+
end
|
59
|
+
|
60
|
+
puts "Duration: #{elapsed} seconds, rate: #{COUNT.to_f/elapsed} docs/sec",
|
61
|
+
'-'*80
|
62
|
+
|
63
|
+
puts "Finding #{COUNT} records one by one from SQLite..."
|
64
|
+
|
65
|
+
elapsed = Benchmark.realtime do
|
66
|
+
(1..COUNT).map { |i| SQLArticle.find(i) }
|
67
|
+
end
|
68
|
+
|
69
|
+
puts "Duration: #{elapsed} seconds, rate: #{COUNT.to_f/elapsed} docs/sec",
|
70
|
+
'-'*80
|
71
|
+
|
72
|
+
puts "Updating all documents in batches of 1000 in SQLite..."
|
73
|
+
|
74
|
+
elapsed = Benchmark.realtime do
|
75
|
+
SQLArticle.find_each { |document| document.title += ' (touched)' and document.save }
|
76
|
+
end
|
77
|
+
|
78
|
+
puts "Duration: #{elapsed} seconds, rate: #{COUNT.to_f/elapsed} docs/sec"
|
79
|
+
|
80
|
+
puts "", '='*80, ""
|
81
|
+
sleep 5
|
82
|
+
|
21
83
|
puts "Saving #{COUNT} documents into Redis..."
|
22
84
|
|
23
85
|
elapsed = Benchmark.realtime do
|
@@ -29,6 +91,15 @@ end
|
|
29
91
|
puts "Duration: #{elapsed} seconds, rate: #{COUNT.to_f/elapsed} docs/sec",
|
30
92
|
'-'*80
|
31
93
|
|
94
|
+
puts "Finding all documents..."
|
95
|
+
|
96
|
+
elapsed = Benchmark.realtime do
|
97
|
+
Article.all
|
98
|
+
end
|
99
|
+
|
100
|
+
puts "Duration: #{elapsed} seconds, rate: #{COUNT.to_f/elapsed} docs/sec",
|
101
|
+
'-'*80
|
102
|
+
|
32
103
|
puts "Finding #{COUNT} documents one by one..."
|
33
104
|
|
34
105
|
elapsed = Benchmark.realtime do
|
@@ -55,8 +126,9 @@ elapsed = Benchmark.realtime do
|
|
55
126
|
Article.find_each { |document| document.title += ' (touched)' and document.save }
|
56
127
|
end
|
57
128
|
|
58
|
-
puts "Duration: #{elapsed} seconds, rate: #{COUNT.to_f/elapsed} docs/sec"
|
59
|
-
|
129
|
+
puts "Duration: #{elapsed} seconds, rate: #{COUNT.to_f/elapsed} docs/sec"
|
130
|
+
|
131
|
+
puts '='*80
|
60
132
|
|
61
133
|
__END__
|
62
134
|
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
|
data/lib/redis/persistence.rb
CHANGED
@@ -49,6 +49,7 @@ class Redis
|
|
49
49
|
extend ActiveSupport::Concern
|
50
50
|
|
51
51
|
class RedisNotAvailable < StandardError; end
|
52
|
+
class FamilyNotLoaded < StandardError; end
|
52
53
|
|
53
54
|
DEFAULT_FAMILY = 'default'
|
54
55
|
|
@@ -85,7 +86,7 @@ class Redis
|
|
85
86
|
include ActiveModel::Conversion
|
86
87
|
|
87
88
|
extend ActiveModel::Callbacks
|
88
|
-
define_model_callbacks :save, :destroy
|
89
|
+
define_model_callbacks :save, :destroy, :create
|
89
90
|
end
|
90
91
|
end
|
91
92
|
|
@@ -118,14 +119,18 @@ class Redis
|
|
118
119
|
def property(name, options = {})
|
119
120
|
# Getter method
|
120
121
|
#
|
121
|
-
attr_reader name.to_sym
|
122
|
+
# attr_reader name.to_sym
|
123
|
+
define_method("#{name}") do
|
124
|
+
raise FamilyNotLoaded, "You are accessing the '#{name}' property in the '#{self.class.property_families[name.to_s]}' family which was not loaded.\nTo prevent you from losing data, this exception was raised. Consider loading the model with the family:\n\n #{self.class.to_s}.find('#{self.id}', families: '#{self.class.property_families[name.to_s]}')\n\n" if self.persisted? and not self.__loaded_families.include?( self.class.property_families[name.to_s] )
|
125
|
+
instance_variable_get(:"@#{name}")
|
126
|
+
end
|
122
127
|
|
123
128
|
# Setter method
|
124
129
|
#
|
125
130
|
define_method("#{name}=") do |value|
|
126
131
|
# When changing property, update also loaded family:
|
127
132
|
if instance_variable_get(:"@#{name}") != value && self.class.property_defaults[name.to_sym] != value
|
128
|
-
self.__loaded_families |= self.class.
|
133
|
+
self.__loaded_families |= self.class.family_properties.invert.map do |key, value|
|
129
134
|
value.to_s if key.map(&:to_s).include?(name.to_s)
|
130
135
|
end.compact
|
131
136
|
end
|
@@ -143,8 +148,12 @@ class Redis
|
|
143
148
|
property_types[name.to_sym] = options[:class] if options[:class]
|
144
149
|
|
145
150
|
# Save the property in corresponding family:
|
146
|
-
if options[:family]
|
147
|
-
|
151
|
+
if options[:family]
|
152
|
+
(family_properties[options[:family].to_sym] ||= []) << name.to_s
|
153
|
+
property_families[name.to_s] = options[:family].to_s
|
154
|
+
else
|
155
|
+
(family_properties[DEFAULT_FAMILY.to_sym] ||= []) << name.to_s
|
156
|
+
property_families[name.to_s] = DEFAULT_FAMILY.to_s
|
148
157
|
end
|
149
158
|
|
150
159
|
self
|
@@ -168,10 +177,16 @@ class Redis
|
|
168
177
|
@property_types ||= {}
|
169
178
|
end
|
170
179
|
|
171
|
-
# Returns a Hash mapping
|
180
|
+
# Returns a Hash mapping properties to families
|
172
181
|
#
|
173
182
|
def property_families
|
174
|
-
@property_families ||= {
|
183
|
+
@property_families ||= { 'id' => DEFAULT_FAMILY.to_s }
|
184
|
+
end
|
185
|
+
|
186
|
+
# Returns a Hash mapping families to properties
|
187
|
+
#
|
188
|
+
def family_properties
|
189
|
+
@family_properties ||= { DEFAULT_FAMILY.to_sym => ['id'] }
|
175
190
|
end
|
176
191
|
|
177
192
|
# Find one or multiple records
|
@@ -205,7 +220,7 @@ class Redis
|
|
205
220
|
end
|
206
221
|
|
207
222
|
def __find_one(id, options={})
|
208
|
-
families = options[:families] == 'all' ?
|
223
|
+
families = options[:families] == 'all' ? family_properties.keys.map(&:to_s) : [DEFAULT_FAMILY.to_s] | Array(options[:families])
|
209
224
|
data = __redis.hmget("#{self.model_name.plural}:#{id}", *families)
|
210
225
|
|
211
226
|
unless data.compact.empty?
|
@@ -221,7 +236,7 @@ class Redis
|
|
221
236
|
end
|
222
237
|
|
223
238
|
def __find_all(options={})
|
224
|
-
__find_many __all_ids
|
239
|
+
__find_many __all_ids, options
|
225
240
|
end
|
226
241
|
|
227
242
|
# Find all records in the database:
|
@@ -246,33 +261,43 @@ class Redis
|
|
246
261
|
|
247
262
|
def initialize(attributes={})
|
248
263
|
# Store "loaded_families" based on passed attributes, for using when saving:
|
249
|
-
self.class.
|
264
|
+
self.class.family_properties.each do |name, properties|
|
250
265
|
self.__loaded_families |= [name.to_s] if ( properties.map(&:to_s) & attributes.keys.map(&:to_s) ).size > 0
|
251
266
|
end
|
252
267
|
|
253
268
|
# Make copy of objects in the property defaults hash (so default values are left intact):
|
254
|
-
property_defaults = self.class.property_defaults.inject({}) do |
|
269
|
+
property_defaults = self.class.property_defaults.inject({}) do |hash, item|
|
255
270
|
key, value = item
|
256
|
-
|
257
|
-
|
271
|
+
hash[key] = value.class.respond_to?(:new) ? value.clone : value
|
272
|
+
hash
|
258
273
|
end
|
259
274
|
|
275
|
+
# Update attributes, respecting defaults:
|
260
276
|
__update_attributes property_defaults.merge(attributes)
|
261
277
|
self
|
262
278
|
end; alias :attributes= :initialize
|
263
279
|
|
280
|
+
# Update record attributes and save it:
|
281
|
+
#
|
282
|
+
# article.update_attributes title: 'Changed', published: true, ...
|
283
|
+
#
|
264
284
|
def update_attributes(attributes={})
|
265
285
|
__update_attributes attributes
|
266
286
|
save
|
267
287
|
self
|
268
288
|
end
|
269
289
|
|
270
|
-
# Returns
|
290
|
+
# Returns record attributes as a Hash.
|
271
291
|
#
|
272
292
|
def attributes
|
273
293
|
self.class.
|
274
294
|
properties.
|
275
|
-
inject({})
|
295
|
+
inject({}) do |attributes, key|
|
296
|
+
begin
|
297
|
+
attributes[key] = send(key)
|
298
|
+
rescue FamilyNotLoaded; end
|
299
|
+
attributes
|
300
|
+
end
|
276
301
|
end
|
277
302
|
|
278
303
|
# Saves the record in the database, performing callbacks:
|
@@ -292,24 +317,51 @@ class Redis
|
|
292
317
|
# Be careful not to overwrite properties with default values.
|
293
318
|
#
|
294
319
|
def save(options={})
|
295
|
-
|
320
|
+
perform = lambda do
|
296
321
|
self.id ||= self.class.__next_id
|
297
|
-
families = if options[:families] == 'all'; self.class.
|
322
|
+
families = if options[:families] == 'all'; self.class.family_properties.keys
|
298
323
|
else; self.__loaded_families | Array(options[:families])
|
299
324
|
end
|
300
325
|
params = families.map do |family|
|
301
|
-
[family.to_s, self.to_json(:only => self.class.
|
326
|
+
[family.to_s, self.to_json(:only => self.class.family_properties[family.to_sym])]
|
302
327
|
end.flatten
|
303
|
-
__redis.hmset
|
328
|
+
__redis.hmset __redis_key, *params
|
329
|
+
end
|
330
|
+
run_callbacks :save do
|
331
|
+
unless persisted?
|
332
|
+
run_callbacks :create do
|
333
|
+
perform.()
|
334
|
+
end
|
335
|
+
else
|
336
|
+
perform.()
|
337
|
+
end
|
304
338
|
end
|
305
339
|
self
|
306
340
|
end
|
307
341
|
|
308
|
-
#
|
342
|
+
# Reloads the model, updating its loaded families and attributes,
|
343
|
+
# eg. when you want to access properties in not-loaded families:
|
344
|
+
#
|
345
|
+
# article.views
|
346
|
+
# # => FamilyNotLoaded
|
347
|
+
# article.reload(families: 'counters').views
|
348
|
+
# # => 100
|
349
|
+
#
|
350
|
+
def reload(options={})
|
351
|
+
families = self.__loaded_families | Array(options[:families])
|
352
|
+
reloaded = self.class.find(self.id, options.merge(families: families))
|
353
|
+
self.attributes = reloaded.attributes
|
354
|
+
self.__loaded_families = reloaded.__loaded_families
|
355
|
+
self
|
356
|
+
end
|
357
|
+
|
358
|
+
# Removes the record from the database, performing callbacks:
|
359
|
+
#
|
360
|
+
# article.destroy
|
309
361
|
#
|
310
362
|
def destroy
|
311
363
|
run_callbacks :destroy do
|
312
|
-
__redis.del
|
364
|
+
__redis.del __redis_key
|
313
365
|
end
|
314
366
|
self.freeze
|
315
367
|
end
|
@@ -317,37 +369,46 @@ class Redis
|
|
317
369
|
# Returns whether record is saved into database
|
318
370
|
#
|
319
371
|
def persisted?
|
320
|
-
__redis.exists
|
372
|
+
__redis.exists __redis_key
|
321
373
|
end
|
322
374
|
|
323
375
|
def inspect
|
324
|
-
"#<#{self.class}: #{attributes}>"
|
376
|
+
"#<#{self.class}: #{attributes}, loaded_families: #{__loaded_families.join(', ')}>"
|
377
|
+
end
|
378
|
+
|
379
|
+
# Returns the composited key for storing the record in the database
|
380
|
+
#
|
381
|
+
def __redis_key
|
382
|
+
"#{self.class.model_name.plural}:#{self.id}"
|
325
383
|
end
|
326
384
|
|
327
|
-
# Updates record properties, taking care of casting to
|
328
|
-
# (single values or collections), augmenting hashes so you can access them with dot notation,
|
329
|
-
# and automatically converting properly formatted time values to Time classes.
|
385
|
+
# Updates record properties, taking care of casting to custom or built-in classes.
|
330
386
|
#
|
331
387
|
def __update_attributes(attributes)
|
332
|
-
attributes.each
|
333
|
-
|
334
|
-
|
388
|
+
attributes.each { |name, value| send "#{name}=", __cast_value(name, value) }
|
389
|
+
end
|
390
|
+
|
391
|
+
# Casts the values according to the <tt>:class</tt> option set when
|
392
|
+
# defining the property, cast Hashes as Hashr[http://rubygems.org/gems/hashr] instances
|
393
|
+
# automatically convert UTC formatted strings to Time.
|
394
|
+
#
|
395
|
+
def __cast_value(name, value)
|
396
|
+
case
|
397
|
+
|
335
398
|
when klass = self.class.property_types[name.to_sym]
|
336
|
-
# ... as an Array ...
|
337
399
|
if klass.is_a?(Array) && value.is_a?(Array)
|
338
|
-
|
339
|
-
# ... or object?
|
400
|
+
value.map { |v| v.class == klass.first ? v : klass.first.new(v) }
|
340
401
|
else
|
341
|
-
|
402
|
+
value.class == klass ? value : klass.new(value)
|
342
403
|
end
|
343
|
-
|
404
|
+
|
344
405
|
when value.is_a?(Hash)
|
345
|
-
|
406
|
+
Hashr.new(value)
|
407
|
+
|
346
408
|
else
|
347
409
|
# Strings formatted as <http://en.wikipedia.org/wiki/ISO8601> are automatically converted to Time
|
348
410
|
value = Time.parse(value) if value.is_a?(String) && value =~ /^\d{4}[\/\-]\d{2}[\/\-]\d{2}T\d{2}\:\d{2}\:\d{2}Z$/
|
349
|
-
|
350
|
-
end
|
411
|
+
value
|
351
412
|
end
|
352
413
|
end
|
353
414
|
|
data/test/models.rb
CHANGED
@@ -39,11 +39,15 @@ class ModelWithCallbacks
|
|
39
39
|
before_save :my_callback_method
|
40
40
|
after_save :my_callback_method
|
41
41
|
before_destroy { @hooked = 'YEAH' }
|
42
|
+
before_create :my_before_create_method
|
42
43
|
|
43
44
|
property :title
|
44
45
|
|
45
46
|
def my_callback_method
|
46
47
|
end
|
48
|
+
|
49
|
+
def my_before_create_method
|
50
|
+
end
|
47
51
|
end
|
48
52
|
|
49
53
|
class ModelWithValidations
|
@@ -104,7 +108,7 @@ end
|
|
104
108
|
class ModelWithCastingInFamily
|
105
109
|
include Redis::Persistence
|
106
110
|
property :pieces, :class => [Piece], :default => [], :family => 'meta'
|
107
|
-
property :parts, :class => [Piece], :default => [], :family => '
|
111
|
+
property :parts, :class => [Piece], :default => [], :family => 'other'
|
108
112
|
end
|
109
113
|
|
110
114
|
class ModelWithDefaultArray
|
@@ -153,7 +153,6 @@ class RedisPersistenceTest < ActiveSupport::TestCase
|
|
153
153
|
|
154
154
|
m = ModelWithFamily.find(1)
|
155
155
|
assert_not_nil m.name
|
156
|
-
assert_nil m.views
|
157
156
|
|
158
157
|
m = ModelWithFamily.find(1, :families => ['counters', 'meta'])
|
159
158
|
assert_not_nil m.name
|
@@ -166,19 +165,28 @@ class RedisPersistenceTest < ActiveSupport::TestCase
|
|
166
165
|
|
167
166
|
should "cast the values" do
|
168
167
|
m = ModelWithCastingInFamily.create pieces: [ { name: 'One', level: 42 } ]
|
169
|
-
m.save
|
170
|
-
|
171
|
-
m = ModelWithCastingInFamily.find(1)
|
172
|
-
assert_equal [], m.pieces
|
173
|
-
assert_equal [], m.parts
|
174
|
-
assert_nil m.pieces.first
|
175
168
|
|
176
169
|
m = ModelWithCastingInFamily.find(1, :families => 'meta')
|
177
|
-
|
170
|
+
assert_raise(Redis::Persistence::FamilyNotLoaded) { m.parts }
|
178
171
|
assert_not_nil m.pieces.first
|
179
172
|
assert_equal 42, m.pieces.first.level
|
180
173
|
end
|
181
174
|
|
175
|
+
should "not cas the value if it already has proper class" do
|
176
|
+
m = ModelWithCasting.create thing: {value: 'foo'}
|
177
|
+
|
178
|
+
m.reload
|
179
|
+
assert_equal 'foo', m.thing.value
|
180
|
+
end
|
181
|
+
|
182
|
+
should "not cast the values if it already has proper class" do
|
183
|
+
m = ModelWithCastingInFamily.create pieces: [ { name: 'One', level: 42 }, { name: 'Two', level: 45 } ]
|
184
|
+
|
185
|
+
m.reload(families: 'meta')
|
186
|
+
assert_equal 42, m.pieces.first.level
|
187
|
+
assert_equal 45, m.pieces.last.level
|
188
|
+
end
|
189
|
+
|
182
190
|
should "store loaded families on initialization" do
|
183
191
|
m = ModelWithCastingInFamily.new pieces: [ { name: 'One', level: 42 } ]
|
184
192
|
assert_equal ['default', 'meta'], m.__loaded_families
|
@@ -239,6 +247,12 @@ class RedisPersistenceTest < ActiveSupport::TestCase
|
|
239
247
|
assert_equal '3', PersistentArticle.all.last.title
|
240
248
|
end
|
241
249
|
|
250
|
+
should "return all instances with desired families" do
|
251
|
+
3.times { |i| ModelWithFamily.create name: "#{i+1}", tags: [ 'foo', 'bar' ] }
|
252
|
+
assert_equal 3, ModelWithFamily.all(families: 'tags').size
|
253
|
+
assert_equal ['foo', 'bar'], ModelWithFamily.all(families: 'tags').last.tags
|
254
|
+
end
|
255
|
+
|
242
256
|
should "return instances by IDs" do
|
243
257
|
10.times { |i| PersistentArticle.create title: "#{i+1}" }
|
244
258
|
assert_equal 10, PersistentArticle.all.size
|
@@ -266,7 +280,6 @@ class RedisPersistenceTest < ActiveSupport::TestCase
|
|
266
280
|
|
267
281
|
m = ModelWithFamily.find(1)
|
268
282
|
assert_equal 1, m.__loaded_families.size
|
269
|
-
assert_nil m.views
|
270
283
|
|
271
284
|
m = ModelWithFamily.find(1, families: 'all')
|
272
285
|
assert_equal 4, m.__loaded_families.size
|
@@ -365,6 +378,37 @@ class RedisPersistenceTest < ActiveSupport::TestCase
|
|
365
378
|
assert_equal 'New', a.title
|
366
379
|
end
|
367
380
|
|
381
|
+
should "reload itself" do
|
382
|
+
m = ModelWithFamily.create
|
383
|
+
assert_raise(Redis::Persistence::FamilyNotLoaded) { m.views }
|
384
|
+
|
385
|
+
m.reload(families: 'counters')
|
386
|
+
assert m.__loaded_families.include?('counters'), m.__loaded_families.inspect
|
387
|
+
assert_nothing_raised { assert_nil m.views }
|
388
|
+
|
389
|
+
m.views = 100
|
390
|
+
m.save
|
391
|
+
|
392
|
+
assert_equal 100, ModelWithFamily.find(1, families: 'counters').views
|
393
|
+
end
|
394
|
+
|
395
|
+
should "reload itself when casted" do
|
396
|
+
m = ModelWithCastingInFamily.create
|
397
|
+
assert_raise(Redis::Persistence::FamilyNotLoaded) { m.pieces }
|
398
|
+
|
399
|
+
m.reload(families: 'meta')
|
400
|
+
assert_equal [], m.pieces
|
401
|
+
end
|
402
|
+
|
403
|
+
should "reload itself with already loaded families" do
|
404
|
+
m = ModelWithCastingInFamily.create pieces: [{name: 'foo'}], parts: [{name: 'bar'}]
|
405
|
+
|
406
|
+
m = ModelWithCastingInFamily.find(m.id, families: 'meta')
|
407
|
+
m = m.reload
|
408
|
+
assert_equal 'foo', m.pieces.first.name
|
409
|
+
assert m.__loaded_families.include?('meta')
|
410
|
+
end
|
411
|
+
|
368
412
|
should "get auto-incrementing ID on save when none is passed" do
|
369
413
|
article = PersistentArticle.new title: 'One'
|
370
414
|
|
@@ -377,6 +421,19 @@ class RedisPersistenceTest < ActiveSupport::TestCase
|
|
377
421
|
assert_equal 2, PersistentArticle.__next_id
|
378
422
|
end
|
379
423
|
|
424
|
+
should "fire before_create hooks" do
|
425
|
+
ModelWithCallbacks.any_instance.expects(:my_before_create_method)
|
426
|
+
|
427
|
+
ModelWithCallbacks.create title: 'Hooks'
|
428
|
+
end
|
429
|
+
|
430
|
+
should "fire both before_create and before_save hooks if defined" do
|
431
|
+
ModelWithCallbacks.any_instance.expects(:my_before_create_method)
|
432
|
+
ModelWithCallbacks.any_instance.expects(:my_callback_method).twice
|
433
|
+
|
434
|
+
ModelWithCallbacks.create title: 'Hooks'
|
435
|
+
end
|
436
|
+
|
380
437
|
should "fire before_save hooks" do
|
381
438
|
article = ModelWithCallbacks.new title: 'Hooks'
|
382
439
|
article.expects(:my_callback_method).twice
|
@@ -418,12 +475,7 @@ class RedisPersistenceTest < ActiveSupport::TestCase
|
|
418
475
|
end
|
419
476
|
|
420
477
|
should "not overwrite properties in not-loaded family with defaults" do
|
421
|
-
m = ModelWithDefaultsInFamilies.
|
422
|
-
m.save
|
423
|
-
|
424
|
-
# Return defaults
|
425
|
-
m = ModelWithDefaultsInFamilies.find(1)
|
426
|
-
assert_equal [], m.tags
|
478
|
+
m = ModelWithDefaultsInFamilies.create :name => 'One'
|
427
479
|
|
428
480
|
# Return defaults
|
429
481
|
m = ModelWithDefaultsInFamilies.find(1, families: 'tags')
|
@@ -438,22 +490,32 @@ class RedisPersistenceTest < ActiveSupport::TestCase
|
|
438
490
|
m = ModelWithDefaultsInFamilies.find(1, :families => 'tags')
|
439
491
|
assert_equal ['foo'], m.tags
|
440
492
|
|
441
|
-
# Return defaults
|
442
|
-
m = ModelWithDefaultsInFamilies.find(1)
|
443
|
-
assert_equal [], m.tags
|
444
|
-
|
445
493
|
# Change another property
|
446
494
|
m = ModelWithDefaultsInFamilies.find(1)
|
447
|
-
m.name = '
|
495
|
+
m.name = 'Changed'
|
448
496
|
m.save
|
449
497
|
|
450
|
-
# Return defaults
|
451
|
-
m = ModelWithDefaultsInFamilies.find(1)
|
452
|
-
assert_equal [], m.tags
|
453
|
-
|
454
498
|
# Return data
|
455
499
|
m = ModelWithDefaultsInFamilies.find(1, :families => 'tags')
|
456
500
|
assert_equal ['foo'], m.tags
|
501
|
+
|
502
|
+
# Return defaults
|
503
|
+
ModelWithDefaultsInFamilies.create :name => 'Two'
|
504
|
+
n = ModelWithDefaultsInFamilies.find(2, :families => 'tags')
|
505
|
+
assert_equal [], n.tags
|
506
|
+
end
|
507
|
+
|
508
|
+
should "raise an exception when accessing not-loaded property on persisted model" do
|
509
|
+
ModelWithFamily.new.save
|
510
|
+
m = ModelWithFamily.find(1)
|
511
|
+
assert_raise(Redis::Persistence::FamilyNotLoaded) { m.lang }
|
512
|
+
end
|
513
|
+
|
514
|
+
should "not raise an exception when accessing property on not-yet-persisted model" do
|
515
|
+
m = ModelWithFamily.new
|
516
|
+
assert_nothing_raised do
|
517
|
+
assert_nil m.lang
|
518
|
+
end
|
457
519
|
end
|
458
520
|
|
459
521
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: redis-persistence
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.4
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -10,11 +10,11 @@ authors:
|
|
10
10
|
autorequire:
|
11
11
|
bindir: bin
|
12
12
|
cert_chain: []
|
13
|
-
date:
|
13
|
+
date: 2012-03-22 00:00:00.000000000 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: activemodel
|
17
|
-
requirement: &
|
17
|
+
requirement: &70133201142020 !ruby/object:Gem::Requirement
|
18
18
|
none: false
|
19
19
|
requirements:
|
20
20
|
- - ~>
|
@@ -22,10 +22,10 @@ dependencies:
|
|
22
22
|
version: '3.0'
|
23
23
|
type: :runtime
|
24
24
|
prerelease: false
|
25
|
-
version_requirements: *
|
25
|
+
version_requirements: *70133201142020
|
26
26
|
- !ruby/object:Gem::Dependency
|
27
27
|
name: multi_json
|
28
|
-
requirement: &
|
28
|
+
requirement: &70133201138700 !ruby/object:Gem::Requirement
|
29
29
|
none: false
|
30
30
|
requirements:
|
31
31
|
- - ~>
|
@@ -33,10 +33,10 @@ dependencies:
|
|
33
33
|
version: '1.0'
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
|
-
version_requirements: *
|
36
|
+
version_requirements: *70133201138700
|
37
37
|
- !ruby/object:Gem::Dependency
|
38
38
|
name: redis
|
39
|
-
requirement: &
|
39
|
+
requirement: &70133201122260 !ruby/object:Gem::Requirement
|
40
40
|
none: false
|
41
41
|
requirements:
|
42
42
|
- - ~>
|
@@ -44,10 +44,10 @@ dependencies:
|
|
44
44
|
version: 2.2.2
|
45
45
|
type: :runtime
|
46
46
|
prerelease: false
|
47
|
-
version_requirements: *
|
47
|
+
version_requirements: *70133201122260
|
48
48
|
- !ruby/object:Gem::Dependency
|
49
49
|
name: hashr
|
50
|
-
requirement: &
|
50
|
+
requirement: &70133201117960 !ruby/object:Gem::Requirement
|
51
51
|
none: false
|
52
52
|
requirements:
|
53
53
|
- - ~>
|
@@ -55,10 +55,10 @@ dependencies:
|
|
55
55
|
version: 0.0.16
|
56
56
|
type: :runtime
|
57
57
|
prerelease: false
|
58
|
-
version_requirements: *
|
58
|
+
version_requirements: *70133201117960
|
59
59
|
- !ruby/object:Gem::Dependency
|
60
60
|
name: bundler
|
61
|
-
requirement: &
|
61
|
+
requirement: &70133201114180 !ruby/object:Gem::Requirement
|
62
62
|
none: false
|
63
63
|
requirements:
|
64
64
|
- - ~>
|
@@ -66,10 +66,10 @@ dependencies:
|
|
66
66
|
version: '1.0'
|
67
67
|
type: :development
|
68
68
|
prerelease: false
|
69
|
-
version_requirements: *
|
69
|
+
version_requirements: *70133201114180
|
70
70
|
- !ruby/object:Gem::Dependency
|
71
71
|
name: yajl-ruby
|
72
|
-
requirement: &
|
72
|
+
requirement: &70133201111840 !ruby/object:Gem::Requirement
|
73
73
|
none: false
|
74
74
|
requirements:
|
75
75
|
- - ~>
|
@@ -77,10 +77,10 @@ dependencies:
|
|
77
77
|
version: 0.8.0
|
78
78
|
type: :development
|
79
79
|
prerelease: false
|
80
|
-
version_requirements: *
|
80
|
+
version_requirements: *70133201111840
|
81
81
|
- !ruby/object:Gem::Dependency
|
82
82
|
name: shoulda
|
83
|
-
requirement: &
|
83
|
+
requirement: &70133201109960 !ruby/object:Gem::Requirement
|
84
84
|
none: false
|
85
85
|
requirements:
|
86
86
|
- - ! '>='
|
@@ -88,10 +88,10 @@ dependencies:
|
|
88
88
|
version: '0'
|
89
89
|
type: :development
|
90
90
|
prerelease: false
|
91
|
-
version_requirements: *
|
91
|
+
version_requirements: *70133201109960
|
92
92
|
- !ruby/object:Gem::Dependency
|
93
93
|
name: mocha
|
94
|
-
requirement: &
|
94
|
+
requirement: &70133201101720 !ruby/object:Gem::Requirement
|
95
95
|
none: false
|
96
96
|
requirements:
|
97
97
|
- - ! '>='
|
@@ -99,10 +99,10 @@ dependencies:
|
|
99
99
|
version: '0'
|
100
100
|
type: :development
|
101
101
|
prerelease: false
|
102
|
-
version_requirements: *
|
102
|
+
version_requirements: *70133201101720
|
103
103
|
- !ruby/object:Gem::Dependency
|
104
104
|
name: turn
|
105
|
-
requirement: &
|
105
|
+
requirement: &70133201100020 !ruby/object:Gem::Requirement
|
106
106
|
none: false
|
107
107
|
requirements:
|
108
108
|
- - ! '>='
|
@@ -110,7 +110,7 @@ dependencies:
|
|
110
110
|
version: '0'
|
111
111
|
type: :development
|
112
112
|
prerelease: false
|
113
|
-
version_requirements: *
|
113
|
+
version_requirements: *70133201100020
|
114
114
|
description: Simple ActiveModel-compatible persistence layer in Redis
|
115
115
|
email:
|
116
116
|
- karmi@karmi.cz
|
@@ -160,7 +160,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
160
160
|
version: '0'
|
161
161
|
requirements: []
|
162
162
|
rubyforge_project:
|
163
|
-
rubygems_version: 1.8.
|
163
|
+
rubygems_version: 1.8.11
|
164
164
|
signing_key:
|
165
165
|
specification_version: 3
|
166
166
|
summary: Simple ActiveModel-compatible persistence layer in Redis
|
@@ -169,3 +169,4 @@ test_files:
|
|
169
169
|
- test/active_model_lint_test.rb
|
170
170
|
- test/models.rb
|
171
171
|
- test/redis_persistence_test.rb
|
172
|
+
has_rdoc:
|