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 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 ActiveModel and based on Redis[http://redis.io],
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 <> comments)
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
- # => Hello World!
52
+ # => The Thing!
51
53
 
52
- article.created.class
53
- # => Time
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`](./blob/master/examples/article.rb) for full example.
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); end
12
- def method_missing(method_name, *arguments); @attributes[method_name]; end
13
- def as_json(*); @attributes; end
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, :default => [], :class => [Comment], :family => 'comments'
34
+ property :comments, default: [], class: [Comment], family: 'comments'
25
35
  end
26
36
 
27
- article = Article.new :title => 'Do Not Blink',
28
- :author => 'Malcom Gladwell',
29
- :body => 'Imagine that I asked you ...',
30
- :created => Time.now.utc
31
- # => #<Article: {"id"=>1, "title"=>"Do Not Blink", ...>
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"=>"Do Not Blink", ...>
44
+ # => #<Article: {"id"=>1, "title"=>"I Work For Banks Now!", ...>
35
45
 
36
46
  p article = Article.find(1)
37
- # => #<Article: {"id"=>1, "title"=>"Do Not Blink", ...>
47
+ # => #<Article: {"id"=>1, "title"=>"I Work For Banks Now!", ...>
38
48
 
39
49
  p article.title
40
- # => "Do Not Blink"
50
+ # => "I Work For Banks Now!"
41
51
 
42
52
  p article.created.year
43
53
  # => 2011
44
54
 
45
- article = Article.new :title => 'In the Beginning Was the Command Line'
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.create :title => 'OMG BLOG!'
71
+ article = Article.new title: 'OMG BLOG!'
56
72
 
57
- p article.comments
73
+ article.comments
58
74
  # => []
59
75
 
60
- article.comments << {:nick => '4chan', :body => 'WHY U NO QUIT?'}
76
+ article.comments << {nick: '4chan', body: 'WHY U NO QUIT?'}
61
77
 
62
- article.comments << Comment.new(:nick => 'h4x0r', :body => 'WHY U NO USE BBS?')
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
- p article.comments
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, :families => 'comments')
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
 
@@ -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
- '-'*80
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.
@@ -1,5 +1,5 @@
1
1
  class Redis
2
2
  module Persistence
3
- VERSION = "0.0.3"
3
+ VERSION = "0.0.4"
4
4
  end
5
5
  end
@@ -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.property_families.invert.map do |key, value|
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]; (property_families[options[:family].to_sym] ||= []) << name.to_s
147
- else; (property_families[DEFAULT_FAMILY.to_sym] ||= []) << name.to_s
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 families to properties
180
+ # Returns a Hash mapping properties to families
172
181
  #
173
182
  def property_families
174
- @property_families ||= { DEFAULT_FAMILY.to_sym => ['id'] }
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' ? property_families.keys : [DEFAULT_FAMILY.to_s] | Array(options[:families])
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.property_families.each do |name, properties|
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 |sum, item|
269
+ property_defaults = self.class.property_defaults.inject({}) do |hash, item|
255
270
  key, value = item
256
- sum[key] = value.class.respond_to?(:new) ? value.clone : value
257
- sum
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 a Hash of attributes for serialization, etc
290
+ # Returns record attributes as a Hash.
271
291
  #
272
292
  def attributes
273
293
  self.class.
274
294
  properties.
275
- inject({}) {|attributes, key| attributes[key] = send(key); attributes}
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
- run_callbacks :save do
320
+ perform = lambda do
296
321
  self.id ||= self.class.__next_id
297
- families = if options[:families] == 'all'; self.class.property_families.keys
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.property_families[family.to_sym])]
326
+ [family.to_s, self.to_json(:only => self.class.family_properties[family.to_sym])]
302
327
  end.flatten
303
- __redis.hmset "#{self.class.model_name.plural}:#{self.id}", *params
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
- # Removes the record from the database, performing callbacks.
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 "#{self.class.model_name.plural}:#{self.id}"
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 "#{self.class.model_name.plural}:#{self.id}"
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 specified classes
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 do |name, value|
333
- case
334
- # Should we cast the value ...
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
- send "#{name}=", value.map { |v| klass.first.new(v) }
339
- # ... or object?
400
+ value.map { |v| v.class == klass.first ? v : klass.first.new(v) }
340
401
  else
341
- send "#{name}=", klass.new(value)
402
+ value.class == klass ? value : klass.new(value)
342
403
  end
343
- # Should we return augmented Hash?
404
+
344
405
  when value.is_a?(Hash)
345
- send "#{name}=", Hashr.new(value)
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
- send "#{name}=", value
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 => 'meta'
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
- assert_equal [], m.parts
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.new :name => 'One'
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 = 'Two'
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.3
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: 2011-11-20 00:00:00.000000000 Z
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: &70189836581020 !ruby/object:Gem::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: *70189836581020
25
+ version_requirements: *70133201142020
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: multi_json
28
- requirement: &70189836580520 !ruby/object:Gem::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: *70189836580520
36
+ version_requirements: *70133201138700
37
37
  - !ruby/object:Gem::Dependency
38
38
  name: redis
39
- requirement: &70189836580060 !ruby/object:Gem::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: *70189836580060
47
+ version_requirements: *70133201122260
48
48
  - !ruby/object:Gem::Dependency
49
49
  name: hashr
50
- requirement: &70189836579600 !ruby/object:Gem::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: *70189836579600
58
+ version_requirements: *70133201117960
59
59
  - !ruby/object:Gem::Dependency
60
60
  name: bundler
61
- requirement: &70189836579140 !ruby/object:Gem::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: *70189836579140
69
+ version_requirements: *70133201114180
70
70
  - !ruby/object:Gem::Dependency
71
71
  name: yajl-ruby
72
- requirement: &70189836578680 !ruby/object:Gem::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: *70189836578680
80
+ version_requirements: *70133201111840
81
81
  - !ruby/object:Gem::Dependency
82
82
  name: shoulda
83
- requirement: &70189836578300 !ruby/object:Gem::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: *70189836578300
91
+ version_requirements: *70133201109960
92
92
  - !ruby/object:Gem::Dependency
93
93
  name: mocha
94
- requirement: &70189836577840 !ruby/object:Gem::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: *70189836577840
102
+ version_requirements: *70133201101720
103
103
  - !ruby/object:Gem::Dependency
104
104
  name: turn
105
- requirement: &70189836577420 !ruby/object:Gem::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: *70189836577420
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.10
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: