redis_assist 0.1.0

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.
@@ -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: