redis-persistence 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
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