redis_assist 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,383 @@
1
+ module RedisAssist
2
+ class Base
3
+ class << self
4
+
5
+ def attr_persist(name, opts={})
6
+ persisted_attrs[name] = opts
7
+
8
+ if opts[:as].eql?(:list)
9
+ define_list(name)
10
+ elsif opts[:as].eql?(:hash)
11
+ define_hash(name)
12
+ else
13
+ define_field(name)
14
+ end
15
+ end
16
+
17
+ def find(ids, opts={})
18
+ ids.is_a?(Array) ? find_by_ids(ids, opts) : find_by_id(ids, opts)
19
+ end
20
+
21
+ # Deprecated finds
22
+ def find_by_id(id, opts={})
23
+ raw_attributes = load_attributes(id)
24
+ return nil unless raw_attributes[id]
25
+ obj = new(id: id, raw_attributes: raw_attributes[id])
26
+ (obj.deleted? && !opts[:deleted].eql?(true)) ? nil : obj
27
+ end
28
+
29
+ def find_by_ids(ids, opts={})
30
+ attrs = load_attributes(*ids)
31
+ raw_attributes = attrs
32
+ ids.each_with_object([]) do |id, instances|
33
+ instance = new(id: id, raw_attributes: raw_attributes[id]) unless raw_attributes[id].nil?
34
+ instances << instance if instance && (!instance.deleted? || opts[:deleted].eql?(true))
35
+ end
36
+ end
37
+
38
+ def create(attrs={})
39
+ roll = new(attrs)
40
+ roll.save ? roll : false
41
+ end
42
+
43
+ def exists?(id)
44
+ redis.exists(key_for(id, :attributes))
45
+ end
46
+
47
+ def update(id, params={})
48
+ record = find(id)
49
+ return false unless record
50
+
51
+ redis.multi do
52
+ params.each do |attr, val|
53
+ if persisted_attrs.include?(attr)
54
+ if fields.keys.include? attr
55
+ transform(:to, attr, val)
56
+ redis.hset(key_for(id, :attributes), attr, transform(:to, attr, val))
57
+ end
58
+
59
+ if lists.keys.include? attr
60
+ redis.del(key_for(id, attr))
61
+ redis.rpush(key_for(id, attr), val) unless val.empty?
62
+ end
63
+
64
+ if hashes.keys.include? attr
65
+ redis.del(key_for(id, attr))
66
+ redis.hmset(key_for(id, attr), *hash_to_redis(val))
67
+ end
68
+ end
69
+ end
70
+ end
71
+
72
+ record.send(:invoke_callback, :after_update)
73
+ end
74
+
75
+ def transform(direction, attr, val)
76
+ transformer = RedisAssist.transforms[persisted_attrs[attr][:as]]
77
+
78
+ if transformer
79
+ transformer.transform(direction, val)
80
+ else
81
+ val || persisted_attrs[attr][:default]
82
+ end
83
+ end
84
+
85
+ def fields
86
+ persisted_attrs.select{|k,v| !(v[:as].eql?(:list) || v[:as].eql?(:hash)) }
87
+ end
88
+
89
+ def lists
90
+ persisted_attrs.select{|k,v| v[:as].eql?(:list) }
91
+ end
92
+
93
+ def hashes
94
+ persisted_attrs.select{|k,v| v[:as].eql?(:hash) }
95
+ end
96
+
97
+ # TODO: Attribute class
98
+ def persisted_attrs
99
+ @persisted_attrs ||= {}
100
+ end
101
+
102
+ def key_for(id, attribute)
103
+ "#{key_prefix}:#{id}:#{attribute}"
104
+ end
105
+
106
+ def key_prefix(val=nil)
107
+ return self.key_prefix = val if val
108
+ return @key_prefix if @key_prefix
109
+ return self.key_prefix = StringHelper.underscore(name)
110
+ end
111
+
112
+ def key_prefix=(val)
113
+ @key_prefix = val
114
+ end
115
+
116
+ def redis
117
+ RedisAssist::Config.redis_client
118
+ end
119
+
120
+ def load_attributes(*ids)
121
+ future_attrs = {}
122
+ attrs = {}
123
+
124
+ # Load all the futures into an organized Hash
125
+ redis.pipelined do |pipe|
126
+ ids.each_with_object(future_attrs) do |id, futures|
127
+ future_lists = {}
128
+ future_hashes = {}
129
+ future_fields = nil
130
+
131
+ lists.each do |name, opts|
132
+ future_lists[name] = pipe.lrange(key_for(id, name), 0, -1)
133
+ end
134
+
135
+ hashes.each do |name, opts|
136
+ future_hashes[name] = pipe.hgetall(key_for(id, name))
137
+ end
138
+
139
+ future_fields = pipe.hmget(key_for(id, :attributes), fields.keys)
140
+
141
+ futures[id] = { lists: future_lists, hashes: future_hashes, fields: future_fields }
142
+ end
143
+ end
144
+
145
+ # Remove the empty futures
146
+ future_attrs = ids.each_with_object({}) do |id, obj|
147
+ obj[id] = future_attrs[id] unless future_attrs[id][:fields].value.select{|v| !v.nil? }.empty?
148
+ end
149
+
150
+ # Map futures to attributes
151
+ future_attrs.each_with_object(attrs) do |kv, obj|
152
+ lists = kv[1][:lists].each_with_object({}){|kv,obj| obj[kv[0]] = kv[1].value unless kv[1].value.nil? }
153
+ hashes = kv[1][:hashes].each_with_object({}){|kv,obj| obj[kv[0]] = kv[1].value unless kv[1].value.nil?}
154
+ fields = Hash[*self.fields.keys.zip(kv[1][:fields].value).flatten]
155
+ obj[kv[0]] = { lists: lists, hashes: hashes, fields: fields }
156
+ end
157
+
158
+ attrs
159
+ end
160
+
161
+ def hash_to_redis(obj)
162
+ obj.each_with_object([]) {|kv,args| args<<kv[0]<<kv[1] }
163
+ end
164
+
165
+ private
166
+
167
+ def define_list(name)
168
+ define_method(name) do
169
+ opts = self.class.persisted_attrs[name]
170
+ self.send("#{name}=", opts[:default]) if !lists[name] && opts[:default]
171
+ lists[name]
172
+ end
173
+
174
+ define_method("#{name}=") do |val|
175
+ raise "RedisAssist: tried to store a #{val.class.name} as Array" unless val.is_a?(Array)
176
+ lists[name] = val
177
+ end
178
+ end
179
+
180
+ def define_hash(name)
181
+ define_method(name) do
182
+ opts = self.class.persisted_attrs[name]
183
+ self.send("#{name}=", opts[:default]) if !hashes[name] && opts[:default]
184
+ hashes[name]
185
+ end
186
+
187
+ define_method("#{name}=") do |val|
188
+ raise "RedisAssist: tried to store a #{val.class.name} as Hash" unless val.is_a?(Hash)
189
+ hashes[name] = val
190
+ end
191
+ end
192
+
193
+ def define_field(name)
194
+ define_method(name) do
195
+ self.class.transform(:from, name, attributes[name])
196
+ end
197
+
198
+ define_method("#{name}=") do |val|
199
+ attributes[name] = self.class.transform(:to, name, val)
200
+ end
201
+ end
202
+ end
203
+
204
+ attr_accessor :attributes
205
+ attr_reader :id, :errors
206
+
207
+ def initialize(attrs={})
208
+ @attributes = {}
209
+ self.lists = {}
210
+ self.hashes = {}
211
+ self.errors = []
212
+
213
+ if attrs[:id]
214
+ self.id = attrs[:id]
215
+ load_attributes(attrs[:raw_attributes])
216
+ end
217
+
218
+ return self if id
219
+
220
+ self.new_record = true
221
+
222
+ invoke_callback(:on_load)
223
+
224
+ self.class.persisted_attrs.keys.each do |name|
225
+ send("#{name}=", attrs[name]) if attrs[name]
226
+ attrs.delete(name)
227
+ end
228
+
229
+ raise "RedisAssist: #{self.class.name} does not support fields: #{attrs.keys.join(', ')}" if attrs.length > 0
230
+ end
231
+
232
+ def saved?
233
+ !!(new_record?.eql?(false) && id)
234
+ end
235
+
236
+ def save
237
+ return false unless valid?
238
+
239
+ invoke_callback(:before_update) unless new_record?
240
+ invoke_callback(:before_create) if new_record?
241
+ invoke_callback(:before_save)
242
+
243
+ # Assing the record with the next available ID
244
+ self.id = generate_id
245
+
246
+ redis.multi do
247
+ # build the arguments to pass to redis hmset
248
+ # and insure the fields are explicitely declared
249
+ attribute_args = hash_to_redis(attributes)
250
+
251
+ redis.hmset(key_for(:attributes), *attribute_args)
252
+
253
+ lists.each do |name, val|
254
+ redis.del(key_for(name))
255
+ redis.rpush(key_for(name), val) if val && !val.empty?
256
+ end
257
+
258
+ hashes.each do |name, val|
259
+ hash_as_args = hash_to_redis(val)
260
+ redis.hmset(key_for(name), *hash_as_args)
261
+ end
262
+ end
263
+
264
+ invoke_callback(:after_save)
265
+ invoke_callback(:after_update) unless new_record?
266
+ invoke_callback(:after_create) if new_record?
267
+
268
+ self
269
+ end
270
+
271
+ def save!
272
+ raise "RedisAssist: save! failed with errors" unless save
273
+ self
274
+ end
275
+
276
+ def valid?
277
+ invoke_callback(:before_validation)
278
+ validate
279
+ errors.empty?
280
+ end
281
+
282
+ def validate; end
283
+
284
+ # TODO: should this be a redis-assist feature?
285
+ def deleted?
286
+ return false unless respond_to?(:deleted_at)
287
+ deleted_at && deleted_at.is_a?(Time)
288
+ end
289
+
290
+ def delete
291
+ if respond_to?(:deleted_at)
292
+ self.deleted_at = Time.now.to_f if respond_to?(:deleted_at)
293
+ save
294
+ else
295
+ redis.pipelined do |pipe|
296
+ pipe.del(key_for(:attributes))
297
+ lists.merge(hashes).each do |name|
298
+ pipe.del(key_for(name))
299
+ end
300
+ end
301
+ end
302
+
303
+ invoke_callback(:after_delete)
304
+ self
305
+ end
306
+
307
+ def new_record?
308
+ !!new_record
309
+ end
310
+
311
+ def redis
312
+ self.class.redis
313
+ end
314
+
315
+ def add_error(field, message)
316
+ errors << { field => message }
317
+ end
318
+
319
+ protected
320
+
321
+
322
+ attr_writer :id, :errors
323
+ attr_accessor :lists, :hashes, :new_record
324
+
325
+ def _on_load; end
326
+
327
+ def _before_validation
328
+ self.errors = []
329
+ end
330
+
331
+ def _before_create
332
+ self.created_at = Time.now.to_f if respond_to?(:created_at)
333
+ end
334
+
335
+ def _before_update
336
+ self.updated_at = Time.now.to_f if respond_to?(:updated_at)
337
+ end
338
+
339
+ def _after_delete
340
+ self.deleted_at = Time.now.to_f if respond_to?(:deleted_at)
341
+ end
342
+
343
+ def _after_create
344
+ self.new_record = false
345
+ end
346
+
347
+ def _before_save; end
348
+ def _after_save; end
349
+ def _after_update; end
350
+
351
+
352
+ private
353
+
354
+
355
+ def invoke_callback(callback)
356
+ send("_#{callback}")
357
+ send(callback) if respond_to? callback
358
+ end
359
+
360
+ def generate_id
361
+ redis.incr("#{self.class.key_prefix}:id_sequence")
362
+ end
363
+
364
+ def key_for(attribute)
365
+ self.class.key_for(id, attribute)
366
+ end
367
+
368
+ def load_attributes(raw_attributes)
369
+ return nil unless raw_attributes
370
+ load_response = self.class.load_attributes(id)
371
+ self.lists = load_response[id][:lists]
372
+ self.hashes = load_response[id][:hashes]
373
+ self.attributes = load_response[id][:fields]
374
+ self.new_record = false
375
+ end
376
+
377
+ ##
378
+ # This converts a hash into args for a redis hmset
379
+ def hash_to_redis(obj)
380
+ self.class.hash_to_redis(obj)
381
+ end
382
+ end
383
+ end
@@ -0,0 +1,15 @@
1
+ module RedisAssist
2
+ class Config
3
+ class << self
4
+ def redis_client
5
+ return @redis_client if @redis_client
6
+ redis_config = { :host => '127.0.0.1', :driver => :hiredis }
7
+ self.redis_client = Redis.new(redis_config)
8
+ end
9
+
10
+ def redis_client=(val)
11
+ @redis_client = val
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,37 @@
1
+ module RedisAssist
2
+ def self.register_transform(transform)
3
+ transforms[transform.key.to_sym] = transform
4
+ end
5
+
6
+ def self.transforms
7
+ @transforms ||= {}
8
+ end
9
+
10
+ class Transform
11
+ def self.inherited(base)
12
+ base.extend ClassMethods
13
+ RedisAssist.register_transform base
14
+ end
15
+
16
+ module ClassMethods
17
+ def key
18
+ StringHelper.underscore(name).gsub(/_transform$/, '').to_sym
19
+ end
20
+
21
+ def from(val)
22
+ val
23
+ end
24
+
25
+ def to(val)
26
+ val
27
+ end
28
+
29
+ def transform(direction, val)
30
+ case direction.to_sym
31
+ when :to then to(val)
32
+ when :from then from(val)
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,5 @@
1
+ class BooleanTransform < RedisAssist::Transform
2
+ def self.from(val)
3
+ val.eql?('true') || val.eql?(true)
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class FloatTransform < RedisAssist::Transform
2
+ def self.from(val)
3
+ val.to_f
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class IntegerTransform < RedisAssist::Transform
2
+ def self.from(val)
3
+ val.to_i
4
+ end
5
+ end
@@ -0,0 +1,9 @@
1
+ class JsonTransform < RedisAssist::Transform
2
+ def self.to(val)
3
+ JSON.generate(val)
4
+ end
5
+
6
+ def self.from(val)
7
+ JSON.parse(val)
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ class TimeTransform < RedisAssist::Transform
2
+ def self.to(val)
3
+ val.to_f
4
+ end
5
+
6
+ def self.from(val)
7
+ (val && !val.eql?('')) ? Time.at(val.to_f) : nil
8
+ end
9
+ end
@@ -0,0 +1,3 @@
1
+ module RedisAssist
2
+ VERSION = '0.1.0' unless defined?(::RedisAssist::VERSION)
3
+ end
@@ -0,0 +1,29 @@
1
+ $LOAD_PATH << File.dirname(__FILE__) unless $LOAD_PATH.include?(File.dirname(__FILE__))
2
+
3
+ require 'time'
4
+ require 'redis'
5
+ require 'uuid'
6
+ require 'json'
7
+ require 'redis_assist/config'
8
+ require 'redis_assist/transform'
9
+ require 'redis_assist/base'
10
+
11
+ # == Setup & Configuration
12
+ # RedisAssist depends on the redis-rb gem to communicate with redis.
13
+ # To configure your redis connection simply pass `RedisAssist` a `Redis` client instance.
14
+ # RedisAssist::Config.redis_client = Redis.new(redis_config)
15
+ module RedisAssist
16
+ module StringHelper
17
+ def self.underscore(str)
18
+ str.gsub(/::/, '/').
19
+ gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
20
+ gsub(/([a-z\d])([A-Z])/,'\1_\2').
21
+ tr("-", "_").
22
+ downcase
23
+ end
24
+ end
25
+ end
26
+
27
+ # require all transforms
28
+ Dir["#{File.dirname(__FILE__)}/redis_assist/transforms/*.rb"].each {|file| require file }
29
+
metadata ADDED
@@ -0,0 +1,144 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: redis_assist
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Tyler Love
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-04-16 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: redis
16
+ requirement: &70264080458040 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 3.0.1
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *70264080458040
25
+ - !ruby/object:Gem::Dependency
26
+ name: hiredis
27
+ requirement: &70264080457500 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ~>
31
+ - !ruby/object:Gem::Version
32
+ version: 0.4.5
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: *70264080457500
36
+ - !ruby/object:Gem::Dependency
37
+ name: uuid
38
+ requirement: &70264080456980 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ~>
42
+ - !ruby/object:Gem::Version
43
+ version: 2.3.6
44
+ type: :runtime
45
+ prerelease: false
46
+ version_requirements: *70264080456980
47
+ - !ruby/object:Gem::Dependency
48
+ name: base62
49
+ requirement: &70264080456440 !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: 0.1.4
55
+ type: :runtime
56
+ prerelease: false
57
+ version_requirements: *70264080456440
58
+ - !ruby/object:Gem::Dependency
59
+ name: bundler
60
+ requirement: &70264080455960 !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ~>
64
+ - !ruby/object:Gem::Version
65
+ version: '1.0'
66
+ type: :development
67
+ prerelease: false
68
+ version_requirements: *70264080455960
69
+ - !ruby/object:Gem::Dependency
70
+ name: rake
71
+ requirement: &70264080455360 !ruby/object:Gem::Requirement
72
+ none: false
73
+ requirements:
74
+ - - ~>
75
+ - !ruby/object:Gem::Version
76
+ version: '0.9'
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: *70264080455360
80
+ - !ruby/object:Gem::Dependency
81
+ name: yard
82
+ requirement: &70264080454760 !ruby/object:Gem::Requirement
83
+ none: false
84
+ requirements:
85
+ - - ~>
86
+ - !ruby/object:Gem::Version
87
+ version: 0.8.6.1
88
+ type: :development
89
+ prerelease: false
90
+ version_requirements: *70264080454760
91
+ - !ruby/object:Gem::Dependency
92
+ name: rspec
93
+ requirement: &70264080453820 !ruby/object:Gem::Requirement
94
+ none: false
95
+ requirements:
96
+ - - ~>
97
+ - !ruby/object:Gem::Version
98
+ version: '2.3'
99
+ type: :development
100
+ prerelease: false
101
+ version_requirements: *70264080453820
102
+ description: Redis persistant object oriented programming
103
+ email:
104
+ - t@tylr.org
105
+ executables: []
106
+ extensions: []
107
+ extra_rdoc_files: []
108
+ files:
109
+ - lib/redis_assist/base.rb
110
+ - lib/redis_assist/config.rb
111
+ - lib/redis_assist/transform.rb
112
+ - lib/redis_assist/transforms/boolean_transform.rb
113
+ - lib/redis_assist/transforms/float_transform.rb
114
+ - lib/redis_assist/transforms/integer_transform.rb
115
+ - lib/redis_assist/transforms/json_transform.rb
116
+ - lib/redis_assist/transforms/time_transform.rb
117
+ - lib/redis_assist/version.rb
118
+ - lib/redis_assist.rb
119
+ homepage: http://github.com/tylr/redis-assist
120
+ licenses: []
121
+ post_install_message:
122
+ rdoc_options: []
123
+ require_paths:
124
+ - lib
125
+ required_ruby_version: !ruby/object:Gem::Requirement
126
+ none: false
127
+ requirements:
128
+ - - ! '>='
129
+ - !ruby/object:Gem::Version
130
+ version: '0'
131
+ required_rubygems_version: !ruby/object:Gem::Requirement
132
+ none: false
133
+ requirements:
134
+ - - ! '>='
135
+ - !ruby/object:Gem::Version
136
+ version: '0'
137
+ requirements: []
138
+ rubyforge_project: redis_assist
139
+ rubygems_version: 1.8.10
140
+ signing_key:
141
+ specification_version: 3
142
+ summary: Persistant object oriented programming with redis
143
+ test_files: []
144
+ has_rdoc: