redis-persistence 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README.markdown CHANGED
@@ -1,50 +1,63 @@
1
1
  Redis Persistence
2
2
  =================
3
3
 
4
- `Redis::Persistence` is a simple persistence layer for Ruby objects, fully compatible with ActiveModel,
5
- and thus easily used both standalone or in a Rails project.
4
+ `Redis::Persistence` is a lightweight object persistence framework,
5
+ fully compatible with ActiveModel and based on Redis[http://redis.io],
6
+ easily used standalone or within Rails applications.
6
7
 
7
- ## Usage ##
8
+ Installation
9
+ ------------
8
10
 
9
- ```ruby
10
- require 'redis/persistence'
11
+ $ gem install redis-persistence
11
12
 
12
- Redis::Persistence.config.redis = Redis.new
13
- # => #<Redis client v2.2.2 connected to redis://127.0.0.1:6379/0 (Redis v2.4.1)>
13
+ Features:
14
+ ---------
14
15
 
15
- class Article
16
- include Redis::Persistence
16
+ * 100% Rails compatibility
17
+ * 100% ActiveModel compatibility: callbacks, validations, serialization, ...
18
+ * No crazy `has_many`-type of semantics
19
+ * Auto-incrementing IDs
20
+ * Defining default values for properties
21
+ * Casting properties as built-in or custom classes
22
+ * Convenient "dot access" to properties (<tt>article.views.today</tt>)
23
+ * Support for "collections" of embedded objects (eg. article <> comments)
24
+ * Automatic conversion of UTC-formatted strings to Time objects
17
25
 
18
- property :id
19
- property :title
20
- property :body
21
- property :author, :default => '(Unknown)'
22
- end
26
+ Basic example
27
+ -------------
23
28
 
24
- article = Article.new :id => 1, :title => 'Lorem Ipsum'
25
- # => #<Article: {"id"=>1, "title"=>"Lorem Ipsum", "body"=>nil, "author"=>"(Unknown)"}>
29
+ ```ruby
30
+ require 'redis/persistence'
26
31
 
27
- article.save
28
- # => #<Article: {"id"=>1, "title"=>"Lorem Ipsum", "body"=>nil, "author"=>"(Unknown)"}>
32
+ Redis::Persistence.config.redis = Redis.new
33
+ # => #<Redis client v2.2.2 connected to redis://127.0.0.1:6379/0 (Redis v2.4.1)>
29
34
 
30
- article = Article.find(1)
31
- # => #<Article: {"id"=>1, "title"=>"Lorem Ipsum", "body"=>nil, "author"=>"(Unknown)"}>
35
+ class Article
36
+ include Redis::Persistence
32
37
 
33
- article.title
34
- # => "Lorem Ipsum"
38
+ property :title
39
+ property :body
40
+ property :author, :default => '(Unknown)'
41
+ property :created
42
+ end
35
43
 
36
- article.author
37
- # => "(Unknown)"
38
- ```
44
+ Article.create title: 'The Thing', body: 'So, in the beginning...', created: Time.now.utc
39
45
 
40
- It comes with the standard feature set of ActiveModel classes: validations, callbacks, serialization,
41
- Rails DOM helpers compatibility, etc.
46
+ article = Article.find(1)
47
+ # => <Article: {"id"=>1, "title"=>"The Thing", ...}>
42
48
 
49
+ article.title
50
+ # => Hello World!
43
51
 
44
- ## Installation ##
52
+ article.created.class
53
+ # => Time
54
+
55
+ article.title = 'The Cabin'
56
+ article.save
57
+ # => <Article: {"id"=>1, "title"=>"The Cabin", ...}>
58
+ ```
45
59
 
46
- git clone git://github.com/Ataxo/redis-persistence.git
47
- rake install
60
+ See the [`examples/article.rb`](./blob/master/examples/article.rb) for full example.
48
61
 
49
62
  -----
50
63
 
@@ -40,7 +40,7 @@ end
40
40
  puts "Duration: #{elapsed} seconds, rate: #{COUNT.to_f/elapsed} docs/sec",
41
41
  '-'*80
42
42
 
43
- puts "Finding first 1000 documents with only 'data' family..."
43
+ puts "Finding first 1000 documents with only 'default' family..."
44
44
 
45
45
  elapsed = Benchmark.realtime do
46
46
  Article.find (1..1000).to_a
@@ -54,44 +54,36 @@ puts '-'*80, ''
54
54
  run "bundle install"
55
55
 
56
56
  puts
57
- say_status "Model", "Adding the Article resource...", :yellow
57
+ say_status "Model", "Generating the Article resource with Redis::Persistence...", :yellow
58
58
  puts '-'*80, ''; sleep 1
59
59
 
60
- generate :scaffold, "Article title:string content:text published:date"
60
+ generate :scaffold, "Article title:string content:text published:boolean --orm=redis_persistence"
61
61
  route "root :to => 'articles#index'"
62
62
 
63
63
  git :add => '.'
64
- git :commit => "-m 'Added the Article resource'"
64
+ git :commit => "-m 'Generated the Article resource\n\n(Redis::Persistence-based model, scaffolded controller/views/tests)'"
65
65
 
66
66
  puts
67
- say_status "Model", "Adding Redis::Persistence into the Article model...", :yellow
67
+ say_status "Initializer", "Adding configuration for Redis::Persistence...", :yellow
68
68
  puts '-'*80, ''; sleep 1
69
69
 
70
- run "rm -f app/models/article.rb"
71
- file 'app/models/article.rb', <<-CODE
72
- class Article
73
- include Redis::Persistence
70
+ # initializer 'redis-persistence.rb', <<-CODE
71
+ # Redis::Persistence.config.redis = Redis.new(:db => 14)
72
+ # CODE
74
73
 
75
- property :title
76
- property :content
77
- property :published
78
- end
79
- CODE
74
+ generate 'redis_persistence:initializer', "14"
80
75
 
81
- initializer 'redis-persistence.rb', <<-CODE
82
- Redis::Persistence.config.redis = Redis.new(:db => 14)
83
- CODE
84
-
85
- git :commit => "-a -m 'Added Redis::Persistence into the Article model, added initializer (Redis DB=14)'"
76
+ git :add => 'config/initializers/redis-persistence.rb'
77
+ git :commit => "-a -m 'Added initializer for Redis::Persistence\n\n(Find your data with `redis-cli -n 14 keys articles:*`)'"
86
78
 
87
79
  puts
88
80
  say_status "Database", "Seeding the database with data...", :yellow
89
81
  puts '-'*80, ''; sleep 0.25
90
82
 
91
- run "rm -rf db/migrate"
92
- run "redis-cli -n 14 flushdb"
93
- run "rm -f db/seeds.rb"
94
- file 'db/seeds.rb', <<-CODE
83
+ remove_file 'db/seeds.rb'
84
+ file 'db/seeds.rb', <<-CODE
85
+ Redis::Persistence.config.redis.flushdb
86
+
95
87
  contents = [
96
88
  'Lorem ipsum dolor sit amet.',
97
89
  'Consectetur adipisicing elit, sed do eiusmod tempor incididunt.',
@@ -102,7 +94,7 @@ contents = [
102
94
 
103
95
  puts "Creating articles..."
104
96
  %w[ One Two Three Four Five ].each_with_index do |title, i|
105
- Article.create title: title, content: contents[i], published: i.days.ago.utc
97
+ Article.create title: title, content: contents[i], published: (rand > 0.5 ? true : false)
106
98
  end
107
99
  CODE
108
100
 
@@ -0,0 +1,18 @@
1
+ module RedisPersistence
2
+ module Generators
3
+
4
+ class InitializerGenerator < Rails::Generators::Base
5
+
6
+ desc "Creates initializer file for redis-persistence in config/initializers."
7
+ argument :database, :type => :string, :default => '14', :optional => true, :banner => "<REDIS DATABASE NUMBER>"
8
+
9
+ source_root File.expand_path("../templates", __FILE__)
10
+
11
+ def create_initializer_file
12
+ template "initializer.rb.tt", "config/initializers/redis-persistence.rb"
13
+ end
14
+
15
+ end
16
+
17
+ end
18
+ end
@@ -0,0 +1 @@
1
+ Redis::Persistence.config.redis = Redis.new(:db => <%= database %>)
@@ -0,0 +1,26 @@
1
+ module RedisPersistence
2
+ module Generators
3
+
4
+ class ModelGenerator < Rails::Generators::NamedBase
5
+
6
+ desc "Creates a Redis::Persistence-based model."
7
+ argument :attributes, :type => :array, :default => [], :banner => "property:type property:type ..."
8
+
9
+ source_root File.expand_path("../templates", __FILE__)
10
+
11
+ check_class_collision
12
+
13
+ def create_model_file
14
+ template "model.rb.tt", File.join("app/models", "#{file_name}.rb")
15
+ end
16
+
17
+ hook_for :test_framework
18
+
19
+ def module_namespacing(&block)
20
+ yield if block
21
+ end unless methods.include?(:module_namespacing)
22
+
23
+ end
24
+
25
+ end
26
+ end
@@ -0,0 +1,9 @@
1
+ <% module_namespacing do -%>
2
+ class <%= class_name %>
3
+ include Redis::Persistence
4
+
5
+ <% attributes.each do |attribute| -%>
6
+ property :<%= attribute.name %>
7
+ <% end -%>
8
+ end
9
+ <% end -%>
@@ -0,0 +1,42 @@
1
+ require 'rails/generators/named_base'
2
+ require 'rails/generators/active_model'
3
+
4
+ module RedisPersistence
5
+ module Generators
6
+
7
+ class ActiveModel < ::Rails::Generators::ActiveModel
8
+ def self.all(klass)
9
+ "#{klass}.all"
10
+ end
11
+
12
+ def self.find(klass, params=nil)
13
+ "#{klass}.find(#{params})"
14
+ end
15
+
16
+ def self.build(klass, params=nil)
17
+ if params
18
+ "#{klass}.new(#{params})"
19
+ else
20
+ "#{klass}.new"
21
+ end
22
+ end
23
+
24
+ def save
25
+ "#{name}.save"
26
+ end
27
+
28
+ def update_attributes(params=nil)
29
+ "#{name}.update_attributes(#{params})"
30
+ end
31
+
32
+ def errors
33
+ "#{name}.errors"
34
+ end
35
+
36
+ def destroy
37
+ "#{name}.destroy"
38
+ end
39
+ end
40
+
41
+ end
42
+ end
@@ -0,0 +1 @@
1
+ require 'redis/persistence'
@@ -5,11 +5,61 @@ require 'active_model'
5
5
  require 'active_support/concern'
6
6
  require 'active_support/configurable'
7
7
 
8
+ require File.expand_path('../persistence/railtie', __FILE__) if defined?(Rails)
9
+
8
10
  class Redis
11
+
12
+ # <b>Redis::Persistence</b> is a lightweight object persistence framework,
13
+ # fully compatible with ActiveModel and based on Redis[http://redis.io].
14
+ #
15
+ # Features:
16
+ #
17
+ # * 100% Rails compatibility
18
+ # * 100% ActiveModel compatibility: callbacks, validations, serialization, ...
19
+ # * No crazy +has_many+-type of semantics
20
+ # * Auto-incrementing IDs
21
+ # * Defining default values for properties
22
+ # * Casting properties as built-in or custom classes
23
+ # * Convenient "dot access" to properties (<tt>article.views.today</tt>)
24
+ # * Support for "collections" of embedded objects (eg. article <> comments)
25
+ # * Automatic conversion of UTC-formatted strings to Time objects
26
+ #
27
+ # Basic example:
28
+ #
29
+ # class Article
30
+ # include Redis::Persistence
31
+ #
32
+ # property :title
33
+ # property :body
34
+ # property :author, :default => '(Unknown)'
35
+ # property :created
36
+ # end
37
+ #
38
+ # Article.create title: 'Hello World!', body: 'So, in the beginning...', created: Time.now.utc
39
+ # article = Article.find(1)
40
+ # # => <Article: {"id"=>1, "title"=>"Hello World!", ...}>
41
+ # article.title
42
+ # # => Hello World!
43
+ # article.created.class
44
+ # # => Time
45
+ #
46
+ # See the <tt>examples/article.rb</tt> for full example.
47
+ #
9
48
  module Persistence
10
- include ActiveSupport::Configurable
11
49
  extend ActiveSupport::Concern
12
50
 
51
+ class RedisNotAvailable < StandardError; end
52
+
53
+ DEFAULT_FAMILY = 'default'
54
+
55
+ def self.config
56
+ @__config ||= Hashr.new
57
+ end
58
+
59
+ def self.configure
60
+ yield config
61
+ end
62
+
13
63
  included do
14
64
  include ActiveModelIntegration
15
65
  self.include_root_in_json = false
@@ -21,7 +71,6 @@ class Redis
21
71
  def __redis
22
72
  self.class.__redis
23
73
  end
24
-
25
74
  end
26
75
 
27
76
  module ActiveModelIntegration
@@ -42,88 +91,175 @@ class Redis
42
91
 
43
92
  module ClassMethods
44
93
 
94
+ # Create new record in database:
95
+ #
96
+ # Article.create title: 'Lorem Ipsum'
97
+ #
45
98
  def create(attributes={})
46
99
  new(attributes).save
47
100
  end
48
101
 
102
+ # Define property in the "default" family:
103
+ #
104
+ # property :title
105
+ # property :author, class: Author
106
+ # property :comments, default: []
107
+ # property :comments, default: [], class: [Comment]
108
+ #
109
+ # Specify a custom "family" for this property:
110
+ #
111
+ # property :views, family: 'counters'
112
+ #
113
+ # Only the "default" family is loaded... by default,
114
+ # for performance and limiting used memory.
115
+ #
116
+ # See more examples in the <tt>test/models.rb</tt> file.
117
+ #
49
118
  def property(name, options = {})
50
- attr_accessor name.to_sym
119
+ # Getter method
120
+ #
121
+ attr_reader name.to_sym
122
+
123
+ # Setter method
124
+ #
125
+ define_method("#{name}=") do |value|
126
+ # When changing property, update also loaded family:
127
+ 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|
129
+ value.to_s if key.map(&:to_s).include?(name.to_s)
130
+ end.compact
131
+ end
132
+ # Store the value in instance variable:
133
+ instance_variable_set(:"@#{name}", value)
134
+ end
135
+
136
+ # Save the property in properties array:
51
137
  properties << name.to_s unless properties.include?(name.to_s)
52
138
 
139
+ # Save property default value (when relevant):
53
140
  property_defaults[name.to_sym] = options[:default] if options[:default]
141
+
142
+ # Save property casting (when relevant):
54
143
  property_types[name.to_sym] = options[:class] if options[:class]
55
- unless options[:family]
56
- (property_families[:data] ||= []) << name.to_s
57
- else
58
- (property_families[options[:family].to_sym] ||= []) << name.to_s
144
+
145
+ # 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
59
148
  end
149
+
60
150
  self
61
151
  end
62
152
 
153
+ # Returns an Array with all properties
154
+ #
63
155
  def properties
64
156
  @properties ||= ['id']
65
157
  end
66
158
 
159
+ # Returns a Hash with property default values
160
+ #
67
161
  def property_defaults
68
162
  @property_defaults ||= {}
69
163
  end
70
164
 
165
+ # Returns a Hash with property casting (classes)
166
+ #
71
167
  def property_types
72
168
  @property_types ||= {}
73
169
  end
74
170
 
171
+ # Returns a Hash mapping families to properties
172
+ #
75
173
  def property_families
76
- @property_families ||= { :data => ['id'] }
174
+ @property_families ||= { DEFAULT_FAMILY.to_sym => ['id'] }
77
175
  end
78
176
 
177
+ # Find one or multiple records
178
+ #
179
+ # Article.find 1
180
+ # Article.find [1, 2, 3]
181
+ #
182
+ # Specify a family (other then "default"):
183
+ #
184
+ # Article.find 1, families: 'counters'
185
+ # Article.find 1, families: ['counters', 'meta']
186
+ #
79
187
  def find(args, options={})
80
188
  args.is_a?(Array) ? __find_many(args, options) : __find_one(args, options)
81
189
  end
82
190
 
191
+ # Yield each record in the database, loading them in batches
192
+ # specified as +batch_size+:
193
+ #
194
+ # Article.find_each do |article|
195
+ # article.title += ' (touched)' and article.save
196
+ # end
197
+ #
198
+ # This method is conveninent for batch manipulations of your entire database.
199
+ #
200
+ def find_each(options={}, &block)
201
+ batch_size = options.delete(:batch_size) || 1000
202
+ __all_ids.each_slice batch_size do |batch|
203
+ __find_many(batch, options).each { |document| yield document }
204
+ end
205
+ end
206
+
83
207
  def __find_one(id, options={})
84
- families = ['data'] | Array(options[:families])
208
+ families = options[:families] == 'all' ? property_families.keys : [DEFAULT_FAMILY.to_s] | Array(options[:families])
85
209
  data = __redis.hmget("#{self.model_name.plural}:#{id}", *families)
86
210
 
87
211
  unless data.compact.empty?
88
- attributes = data.inject({}) { |hash, item| hash.update( MultiJson.decode(item) ); hash }
89
- self.new attributes
212
+ attributes = data.compact.inject({}) { |hash, item| hash.update( MultiJson.decode(item, :symbolize_keys => true) ); hash }
213
+ instance = self.new attributes
214
+ instance.__loaded_families = families
215
+ instance
90
216
  end
91
217
  end
92
218
 
93
- def __find_all(options={})
94
- __find_many __all_ids
95
- end
96
- alias :all :__find_all
97
-
98
219
  def __find_many(ids, options={})
99
220
  ids.map { |id| __find_one(id, options) }.compact
100
221
  end
101
222
 
102
- def find_each(options={}, &block)
103
- options = { :batch_size => 1000 }.update(options)
104
- __all_ids.each_slice options[:batch_size] do |batch|
105
- __find_many(batch).each { |document| yield document }
106
- end
223
+ def __find_all(options={})
224
+ __find_many __all_ids
107
225
  end
108
226
 
227
+ # Find all records in the database:
228
+ #
229
+ # Article.all
230
+ #
231
+ alias :all :__find_all
232
+
109
233
  def __next_id
110
234
  __redis.incr("#{self.model_name.plural}_ids")
111
235
  end
112
236
 
113
237
  def __all_ids
114
- __redis.keys("#{self.model_name.plural}:*").map { |id| id[/:(\d+)$/, 1].to_i }.sort
238
+ __redis.keys("#{self.model_name.plural}:*").map { |id| id[/:(.+)$/, 1] }.sort
115
239
  end
116
240
 
117
241
  end
118
242
 
119
243
  module InstanceMethods
120
244
  attr_accessor :id
245
+ attr_writer :__loaded_families
121
246
 
122
247
  def initialize(attributes={})
123
- __update_attributes self.class.property_defaults.merge(attributes)
248
+ # Store "loaded_families" based on passed attributes, for using when saving:
249
+ self.class.property_families.each do |name, properties|
250
+ self.__loaded_families |= [name.to_s] if ( properties.map(&:to_s) & attributes.keys.map(&:to_s) ).size > 0
251
+ end
252
+
253
+ # 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|
255
+ key, value = item
256
+ sum[key] = value.class.respond_to?(:new) ? value.clone : value
257
+ sum
258
+ end
259
+
260
+ __update_attributes property_defaults.merge(attributes)
124
261
  self
125
- end
126
- alias :attributes= :initialize
262
+ end; alias :attributes= :initialize
127
263
 
128
264
  def update_attributes(attributes={})
129
265
  __update_attributes attributes
@@ -131,23 +267,44 @@ class Redis
131
267
  self
132
268
  end
133
269
 
270
+ # Returns a Hash of attributes for serialization, etc
271
+ #
134
272
  def attributes
135
273
  self.class.
136
274
  properties.
137
275
  inject({}) {|attributes, key| attributes[key] = send(key); attributes}
138
276
  end
139
277
 
140
- def save
278
+ # Saves the record in the database, performing callbacks:
279
+ #
280
+ # Article.new(title: 'Test').save
281
+ #
282
+ # Optionally accepts which families to save:
283
+ #
284
+ # article = Article.find(1, families: 'counters')
285
+ # article.views += 1
286
+ # article.save(families: ['counters', 'meta'])
287
+ #
288
+ # You can also save all families:
289
+ #
290
+ # Article.find(1, families: 'all').update_attributes(title: 'Changed').save(families: 'all')
291
+ #
292
+ # Be careful not to overwrite properties with default values.
293
+ #
294
+ def save(options={})
141
295
  run_callbacks :save do
142
296
  self.id ||= self.class.__next_id
143
- params = self.class.property_families.keys.map do |family|
144
- [family.to_s, self.to_json(:only => self.class.property_families[family])]
297
+ families = options[:families] == 'all' ? self.class.property_families.keys : self.__loaded_families
298
+ params = families.map do |family|
299
+ [family.to_s, self.to_json(:only => self.class.property_families[family.to_sym])]
145
300
  end.flatten
146
301
  __redis.hmset "#{self.class.model_name.plural}:#{self.id}", *params
147
302
  end
148
303
  self
149
304
  end
150
305
 
306
+ # Removes the record from the database, performing callbacks.
307
+ #
151
308
  def destroy
152
309
  run_callbacks :destroy do
153
310
  __redis.del "#{self.class.model_name.plural}:#{self.id}"
@@ -155,6 +312,8 @@ class Redis
155
312
  self.freeze
156
313
  end
157
314
 
315
+ # Returns whether record is saved into database
316
+ #
158
317
  def persisted?
159
318
  __redis.exists "#{self.class.model_name.plural}:#{self.id}"
160
319
  end
@@ -163,25 +322,39 @@ class Redis
163
322
  "#<#{self.class}: #{attributes}>"
164
323
  end
165
324
 
325
+ # Updates record properties, taking care of casting to specified classes
326
+ # (single values or collections), augmenting hashes so you can access them with dot notation,
327
+ # and automatically converting properly formatted time values to Time classes.
328
+ #
166
329
  def __update_attributes(attributes)
167
330
  attributes.each do |name, value|
168
331
  case
332
+ # Should we cast the value ...
169
333
  when klass = self.class.property_types[name.to_sym]
334
+ # ... as an Array ...
170
335
  if klass.is_a?(Array) && value.is_a?(Array)
171
336
  send "#{name}=", value.map { |v| klass.first.new(v) }
337
+ # ... or object?
172
338
  else
173
339
  send "#{name}=", klass.new(value)
174
340
  end
341
+ # Should we return augmented Hash?
175
342
  when value.is_a?(Hash)
176
343
  send "#{name}=", Hashr.new(value)
177
344
  else
178
- # Automatically convert <http://en.wikipedia.org/wiki/ISO8601> formatted strings to Time
345
+ # Strings formatted as <http://en.wikipedia.org/wiki/ISO8601> are automatically converted to Time
179
346
  value = Time.parse(value) if value.is_a?(String) && value =~ /^\d{4}[\/\-]\d{2}[\/\-]\d{2}T\d{2}\:\d{2}\:\d{2}Z$/
180
347
  send "#{name}=", value
181
348
  end
182
349
  end
183
350
  end
184
351
 
352
+ # Returns which families were loaded in the record lifecycle
353
+ #
354
+ def __loaded_families
355
+ @__loaded_families ||= [DEFAULT_FAMILY.to_s]
356
+ end
357
+
185
358
  end
186
359
 
187
360
  end
@@ -0,0 +1,17 @@
1
+ module RedisPersistence
2
+ class Railtie < Rails::Railtie
3
+
4
+ initializer "warn when configuration is missing" do
5
+ config.after_initialize do
6
+ unless ::Redis::Persistence.config.redis
7
+ puts "\n[ERROR!] Redis::Persistence is not configured!", '='*80,
8
+ "Please point `Redis::Persistence.config.redis` to a Redis instance, ",
9
+ "if you actually intend to save some data :)\n\n",
10
+ "You can create an initializer with:\n\n",
11
+ " $ rails generate redis_persistence:initializer\n\n", '-'*80, "\n"
12
+ end
13
+ end
14
+ end
15
+
16
+ end
17
+ end
@@ -1,5 +1,5 @@
1
1
  class Redis
2
2
  module Persistence
3
- VERSION = "0.0.1"
3
+ VERSION = "0.0.2"
4
4
  end
5
5
  end
data/test/models.rb CHANGED
@@ -103,4 +103,20 @@ end
103
103
  class ModelWithCastingInFamily
104
104
  include Redis::Persistence
105
105
  property :pieces, :class => [Piece], :default => [], :family => 'meta'
106
+ property :parts, :class => [Piece], :default => [], :family => 'meta'
107
+ end
108
+
109
+ class ModelWithDefaultArray
110
+ include Redis::Persistence
111
+
112
+ property :accounts, :default => []
113
+ property :options, :default => { :switches => []}
114
+ property :deep, :default => { :one => { :two => { :three => [] } } }
115
+ end
116
+
117
+ class ModelWithDefaultsInFamilies
118
+ include Redis::Persistence
119
+
120
+ property :name
121
+ property :tags, :default => [], :family => 'tags'
106
122
  end
@@ -8,14 +8,40 @@ class RedisPersistenceTest < ActiveSupport::TestCase
8
8
  Redis::Persistence.config.redis
9
9
  end
10
10
 
11
- context "Redis Connection" do
12
-
11
+ context "Configuration" do
12
+
13
+ should "be configurable" do
14
+ assert_respond_to Redis::Persistence, :config
15
+ assert_nothing_raised do
16
+ Redis::Persistence.config.foo = 'bar'
17
+ assert_equal 'bar', Redis::Persistence.config.foo
18
+ end
19
+ end
20
+
21
+ should "be configurable with a block" do
22
+ assert_respond_to Redis::Persistence, :configure
23
+ assert_nothing_raised do
24
+ Redis::Persistence.configure { |config| config.foo = 'bar' }
25
+ assert_equal 'bar', Redis::Persistence.config.foo
26
+ end
27
+ end
28
+
29
+ end
30
+
31
+ context "Redis" do
32
+ teardown { Redis::Persistence.config.redis = Redis.new db: ENV['REDIS_PERSISTENCE_TEST_DATABASE'] || 14 }
33
+
13
34
  should "be set" do
14
35
  assert_nothing_raised { in_redis.info }
15
36
  end
16
-
37
+
38
+ should_eventually "raise error when trying to access it when not configured" do
39
+ Redis::Persistence.config.redis = nil
40
+ assert_raise(Redis::Persistence::RedisNotAvailable) { Redis::Persistence.config.redis }
41
+ end
42
+
17
43
  end
18
-
44
+
19
45
  context "Defining properties" do
20
46
 
21
47
  should "define accessors from attributes" do
@@ -108,12 +134,12 @@ class RedisPersistenceTest < ActiveSupport::TestCase
108
134
 
109
135
  context "Defining properties in families" do
110
136
 
111
- should "store properties in the 'data' family by default" do
137
+ should "store properties in the 'default' family by default" do
112
138
  m = ModelWithFamily.new name: 'One'
113
139
  m.save
114
140
 
115
141
  assert in_redis.exists('model_with_families:1'), in_redis.keys.to_s
116
- assert in_redis.hkeys('model_with_families:1').include?('data')
142
+ assert in_redis.hkeys('model_with_families:1').include?('default')
117
143
  end
118
144
 
119
145
  should "store properties in the correct family" do
@@ -122,7 +148,7 @@ class RedisPersistenceTest < ActiveSupport::TestCase
122
148
 
123
149
  assert_equal 1, m.id
124
150
  assert in_redis.exists('model_with_families:1'), in_redis.keys.to_s
125
- assert in_redis.hkeys('model_with_families:1').include?('data'), in_redis.hkeys('model_with_families:1').to_s
151
+ assert in_redis.hkeys('model_with_families:1').include?('default'), in_redis.hkeys('model_with_families:1').to_s
126
152
  assert in_redis.hkeys('model_with_families:1').include?('counters'), in_redis.hkeys('model_with_families:1').to_s
127
153
 
128
154
  m = ModelWithFamily.find(1)
@@ -144,13 +170,43 @@ class RedisPersistenceTest < ActiveSupport::TestCase
144
170
 
145
171
  m = ModelWithCastingInFamily.find(1)
146
172
  assert_equal [], m.pieces
173
+ assert_equal [], m.parts
147
174
  assert_nil m.pieces.first
148
175
 
149
176
  m = ModelWithCastingInFamily.find(1, :families => 'meta')
177
+ assert_equal [], m.parts
150
178
  assert_not_nil m.pieces.first
151
179
  assert_equal 42, m.pieces.first.level
152
180
  end
153
181
 
182
+ should "store loaded families on initialization" do
183
+ m = ModelWithCastingInFamily.new pieces: [ { name: 'One', level: 42 } ]
184
+ assert_equal ['default', 'meta'], m.__loaded_families
185
+
186
+ m = ModelWithCastingInFamily.new
187
+ assert_equal ['default'], m.__loaded_families
188
+ end
189
+
190
+ should "store loaded families on find" do
191
+ ModelWithCastingInFamily.create pieces: [ { name: 'One', level: 42 } ]
192
+ m = ModelWithCastingInFamily.find(1)
193
+ assert_equal ['default'], m.__loaded_families
194
+
195
+ ModelWithCastingInFamily.create pieces: [ { name: 'One', level: 42 } ]
196
+ m = ModelWithCastingInFamily.find(1, families: 'meta')
197
+ assert_equal ['default', 'meta'], m.__loaded_families
198
+ end
199
+
200
+ should "update loaded families on property assignment" do
201
+ m = ModelWithFamily.new name: 'Test'
202
+ assert_equal ['default'], m.__loaded_families
203
+ assert_equal 'Test', m.name
204
+
205
+ m.views = 100
206
+ assert_equal ['default', 'counters'], m.__loaded_families
207
+ assert_equal 100, m.views
208
+ end
209
+
154
210
  end
155
211
 
156
212
  context "Class" do
@@ -205,6 +261,20 @@ class RedisPersistenceTest < ActiveSupport::TestCase
205
261
  assert_match /touched/, PersistentArticle.find(1).title
206
262
  end
207
263
 
264
+ should "load all families" do
265
+ ModelWithFamily.create name: 'One', views: 10, lang: 'en'
266
+
267
+ m = ModelWithFamily.find(1)
268
+ assert_equal 1, m.__loaded_families.size
269
+ assert_nil m.views
270
+
271
+ m = ModelWithFamily.find(1, families: 'all')
272
+ assert_equal 3, m.__loaded_families.size
273
+ assert_equal 'One', m.name
274
+ assert_equal 10, m.views
275
+ assert_equal 'en', m.lang
276
+ end
277
+
208
278
  end
209
279
 
210
280
  context "Instance" do
@@ -224,24 +294,57 @@ class RedisPersistenceTest < ActiveSupport::TestCase
224
294
  end
225
295
 
226
296
  should "be saved and found in Redis" do
227
- article = PersistentArticle.new id: 1, title: 'One'
297
+ article = PersistentArticle.new title: 'One'
228
298
  assert article.save
229
299
  assert in_redis.exists("persistent_articles:1")
230
300
 
231
- assert PersistentArticle.find(1)
301
+ assert_equal 1, PersistentArticle.all.size
302
+ assert_not_nil PersistentArticle.find(1)
232
303
  assert in_redis.keys.size > 0, 'Key not saved into Redis?'
233
304
  assert_equal 'One', PersistentArticle.find(1).title
234
305
  end
235
306
 
236
307
  should "be deleted from Redis" do
237
- article = PersistentArticle.new id: 1, title: 'One'
308
+ article = PersistentArticle.new title: 'One'
238
309
  assert article.save
310
+ keys_count = in_redis.keys.size
311
+
239
312
  assert_not_nil PersistentArticle.find(1)
240
- assert in_redis.keys.size > 0, 'Key not saved into Redis?'
313
+ assert keys_count > 0, 'Key not saved into Redis?'
241
314
 
242
315
  article.destroy
243
316
  assert_nil PersistentArticle.find(1)
244
- assert_equal 0, in_redis.keys.size, 'Key not removed from Redis?'
317
+ assert in_redis.keys.size < keys_count, 'Key not removed from Redis?'
318
+ end
319
+
320
+ should "be saved, found and deleted with an arbitrary ID" do
321
+ article = PersistentArticle.new id: 'abc123', title: 'Special'
322
+ assert article.save
323
+ keys_count = in_redis.keys.size
324
+
325
+ assert_equal 1, PersistentArticle.all.size
326
+ assert_not_nil PersistentArticle.find('abc123')
327
+
328
+ assert keys_count > 0, 'Key not saved into Redis?'
329
+ assert_equal 'Special', PersistentArticle.find('abc123').title
330
+
331
+ assert article.destroy
332
+ assert_equal 0, PersistentArticle.all.size
333
+ assert_nil PersistentArticle.find('abc123')
334
+ assert in_redis.keys.size < keys_count, 'Key not removed from Redis?'
335
+ end
336
+
337
+ should "save all families" do
338
+ m = ModelWithFamily.new name: 'Test'
339
+ assert m.save
340
+ assert_equal 1, in_redis.hkeys("model_with_families:1").size
341
+
342
+ m = ModelWithFamily.new name: 'Test'
343
+ assert m.save(families: 'all')
344
+ assert in_redis.keys.size > 0, 'Key not saved into Redis?'
345
+
346
+ # ["default", "counters", "meta"]
347
+ assert_equal 3, in_redis.hkeys("model_with_families:2").size
245
348
  end
246
349
 
247
350
  should "update attributes" do
@@ -288,6 +391,63 @@ class RedisPersistenceTest < ActiveSupport::TestCase
288
391
  assert_equal 1, m.errors.to_a.size
289
392
  end
290
393
 
394
+ should "not change default value when assigning property" do
395
+ m = ModelWithDefaultArray.new
396
+
397
+ m.accounts << "account_1"
398
+ m.options[:switches] << "switch_1"
399
+ m.deep[:one][:two][:three] << 'foo'
400
+ m.deep.one.two.three << 'four'
401
+
402
+ assert_equal [], ModelWithDefaultArray.new.accounts
403
+ assert_equal [], ModelWithDefaultArray.new.options[:switches]
404
+ assert_equal [], ModelWithDefaultArray.new.deep[:one][:two][:three]
405
+ assert_equal [], ModelWithDefaultArray.new.deep.one.two.three
406
+
407
+ assert_equal [], ModelWithDefaultArray.property_defaults[:accounts]
408
+ assert_equal [], ModelWithDefaultArray.property_defaults[:options][:switches]
409
+ assert_equal [], ModelWithDefaultArray.property_defaults[:deep][:one][:two][:three]
410
+ end
411
+
412
+ should "not overwrite properties in not-loaded family with defaults" do
413
+ m = ModelWithDefaultsInFamilies.new :name => 'One'
414
+ m.save
415
+
416
+ # Return defaults
417
+ m = ModelWithDefaultsInFamilies.find(1)
418
+ assert_equal [], m.tags
419
+
420
+ # Return defaults
421
+ m = ModelWithDefaultsInFamilies.find(1, families: 'tags')
422
+ assert_equal [], m.tags
423
+
424
+ # Add tag
425
+ m = ModelWithDefaultsInFamilies.find(1, families: 'tags')
426
+ m.tags << 'foo'
427
+ m.save
428
+
429
+ # Return data
430
+ m = ModelWithDefaultsInFamilies.find(1, :families => 'tags')
431
+ assert_equal ['foo'], m.tags
432
+
433
+ # Return defaults
434
+ m = ModelWithDefaultsInFamilies.find(1)
435
+ assert_equal [], m.tags
436
+
437
+ # Change another property
438
+ m = ModelWithDefaultsInFamilies.find(1)
439
+ m.name = 'Two'
440
+ m.save
441
+
442
+ # Return defaults
443
+ m = ModelWithDefaultsInFamilies.find(1)
444
+ assert_equal [], m.tags
445
+
446
+ # Return data
447
+ m = ModelWithDefaultsInFamilies.find(1, :families => 'tags')
448
+ assert_equal ['foo'], m.tags
449
+ end
450
+
291
451
  end
292
452
 
293
453
  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.1
4
+ version: 0.0.2
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-10 00:00:00.000000000 Z
13
+ date: 2011-11-20 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: activemodel
17
- requirement: &70276358101260 !ruby/object:Gem::Requirement
17
+ requirement: &70111331651120 !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: *70276358101260
25
+ version_requirements: *70111331651120
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: multi_json
28
- requirement: &70276358100760 !ruby/object:Gem::Requirement
28
+ requirement: &70111331650560 !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: *70276358100760
36
+ version_requirements: *70111331650560
37
37
  - !ruby/object:Gem::Dependency
38
38
  name: redis
39
- requirement: &70276358100300 !ruby/object:Gem::Requirement
39
+ requirement: &70111331650040 !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: *70276358100300
47
+ version_requirements: *70111331650040
48
48
  - !ruby/object:Gem::Dependency
49
49
  name: hashr
50
- requirement: &70276358099840 !ruby/object:Gem::Requirement
50
+ requirement: &70111331649460 !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: *70276358099840
58
+ version_requirements: *70111331649460
59
59
  - !ruby/object:Gem::Dependency
60
60
  name: bundler
61
- requirement: &70276358099380 !ruby/object:Gem::Requirement
61
+ requirement: &70111331648800 !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: *70276358099380
69
+ version_requirements: *70111331648800
70
70
  - !ruby/object:Gem::Dependency
71
71
  name: yajl-ruby
72
- requirement: &70276358098920 !ruby/object:Gem::Requirement
72
+ requirement: &70111331647980 !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: *70276358098920
80
+ version_requirements: *70111331647980
81
81
  - !ruby/object:Gem::Dependency
82
82
  name: shoulda
83
- requirement: &70276358098540 !ruby/object:Gem::Requirement
83
+ requirement: &70111331647400 !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: *70276358098540
91
+ version_requirements: *70111331647400
92
92
  - !ruby/object:Gem::Dependency
93
93
  name: mocha
94
- requirement: &70276352583600 !ruby/object:Gem::Requirement
94
+ requirement: &70111331646780 !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: *70276352583600
102
+ version_requirements: *70111331646780
103
103
  - !ruby/object:Gem::Dependency
104
104
  name: turn
105
- requirement: &70276352974040 !ruby/object:Gem::Requirement
105
+ requirement: &70111331645860 !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: *70276352974040
113
+ version_requirements: *70111331645860
114
114
  description: Simple ActiveModel-compatible persistence layer in Redis
115
115
  email:
116
116
  - karmi@karmi.cz
@@ -126,7 +126,14 @@ files:
126
126
  - examples/article.rb
127
127
  - examples/benchmark.rb
128
128
  - examples/rails-template.rb
129
+ - lib/rails/generators/redis_persistence/initializer/initializer_generator.rb
130
+ - lib/rails/generators/redis_persistence/initializer/templates/initializer.rb.tt
131
+ - lib/rails/generators/redis_persistence/model/model_generator.rb
132
+ - lib/rails/generators/redis_persistence/model/templates/model.rb.tt
133
+ - lib/rails/generators/redis_persistence_generator.rb
134
+ - lib/redis-persistence.rb
129
135
  - lib/redis/persistence.rb
136
+ - lib/redis/persistence/railtie.rb
130
137
  - lib/redis/persistence/version.rb
131
138
  - redis-persistence.gemspec
132
139
  - test/_helper.rb