redis_object 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. data/.coveralls.yml +1 -0
  2. data/.gitignore +6 -0
  3. data/.travis.yml +5 -0
  4. data/Gemfile +8 -0
  5. data/README.markdown +179 -0
  6. data/Rakefile +10 -0
  7. data/lib/redis_object.rb +47 -0
  8. data/lib/redis_object/base.rb +408 -0
  9. data/lib/redis_object/collection.rb +388 -0
  10. data/lib/redis_object/defaults.rb +42 -0
  11. data/lib/redis_object/experimental/history.rb +49 -0
  12. data/lib/redis_object/ext/benchmark.rb +34 -0
  13. data/lib/redis_object/ext/cleaner.rb +14 -0
  14. data/lib/redis_object/ext/filters.rb +68 -0
  15. data/lib/redis_object/ext/script_cache.rb +92 -0
  16. data/lib/redis_object/ext/shardable.rb +18 -0
  17. data/lib/redis_object/ext/triggers.rb +101 -0
  18. data/lib/redis_object/ext/view_caching.rb +258 -0
  19. data/lib/redis_object/ext/views.rb +102 -0
  20. data/lib/redis_object/external_index.rb +25 -0
  21. data/lib/redis_object/indices.rb +97 -0
  22. data/lib/redis_object/inheritance_tracking.rb +23 -0
  23. data/lib/redis_object/keys.rb +37 -0
  24. data/lib/redis_object/storage.rb +93 -0
  25. data/lib/redis_object/storage/adapter.rb +46 -0
  26. data/lib/redis_object/storage/aws.rb +71 -0
  27. data/lib/redis_object/storage/mysql.rb +47 -0
  28. data/lib/redis_object/storage/redis.rb +119 -0
  29. data/lib/redis_object/timestamps.rb +74 -0
  30. data/lib/redis_object/tpl.rb +17 -0
  31. data/lib/redis_object/types.rb +276 -0
  32. data/lib/redis_object/validation.rb +89 -0
  33. data/lib/redis_object/version.rb +5 -0
  34. data/redis_object.gemspec +26 -0
  35. data/spec/adapter_spec.rb +43 -0
  36. data/spec/base_spec.rb +90 -0
  37. data/spec/benchmark_spec.rb +46 -0
  38. data/spec/collections_spec.rb +144 -0
  39. data/spec/defaults_spec.rb +56 -0
  40. data/spec/filters_spec.rb +29 -0
  41. data/spec/indices_spec.rb +45 -0
  42. data/spec/rename_class_spec.rb +96 -0
  43. data/spec/spec_helper.rb +38 -0
  44. data/spec/timestamp_spec.rb +28 -0
  45. data/spec/trigger_spec.rb +51 -0
  46. data/spec/types_spec.rb +103 -0
  47. data/spec/view_caching_spec.rb +130 -0
  48. data/spec/views_spec.rb +72 -0
  49. metadata +172 -0
data/.coveralls.yml ADDED
@@ -0,0 +1 @@
1
+ repo_token: 8vq1j1WGB56M8h1zI6TeuK7x3HVJ1K8l4
data/.gitignore ADDED
@@ -0,0 +1,6 @@
1
+ *.gem
2
+ .bundle
3
+ .DS_Store
4
+ Gemfile.lock
5
+ pkg/*
6
+ coverage/
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ env:
5
+ - "TEST_DB=14"
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ source "http://rubygems.org"
2
+
3
+ gem 'rake'
4
+ gem 'rspec'
5
+ gem 'coveralls', require: false
6
+
7
+ # Specify your gem's dependencies in redis_object.gemspec
8
+ gemspec
data/README.markdown ADDED
@@ -0,0 +1,179 @@
1
+ # RedisObject
2
+ RedisObject is a fast and simple-to-use object persistence layer for Ruby.
3
+
4
+ [![Build Status](https://travis-ci.org/remotezygote/RedisObject.png?branch=master)](https://travis-ci.org/remotezygote/RedisObject)
5
+ [![Coverage Status](https://coveralls.io/repos/remotezygote/RedisObject/badge.png?branch=master)](https://coveralls.io/r/remotezygote/RedisObject?branch=master)
6
+
7
+ ## Prerequisites
8
+ You'll need [Redis](http://redis.io). Other storage adapters are in the works. Maybe.
9
+
10
+
11
+ ## Installation
12
+
13
+ gem install redis_object
14
+
15
+ Or, you can add it to your Gemfile:
16
+
17
+ gem 'redis_object'
18
+
19
+
20
+ ## Usage
21
+ ### Simple Example
22
+ ```ruby
23
+ class Thing < RedisObject
24
+ def name
25
+ "#{first_name} #{last_name}"
26
+ end
27
+ def name=(new_name)
28
+ first, last = new_name.split(" ")
29
+ set(:first_name,first)
30
+ set(:last_name,last)
31
+ end
32
+ end
33
+ a = Thing.create("an_id")
34
+ a.name = "Testy Testerton"
35
+ b = Thing.create({:first_name => "Testy", :last_name => "Testerton"})
36
+ ```
37
+
38
+ ### Config
39
+ You can configure the storage adapter by sending a packet of commands to `configure_store` like:
40
+
41
+ ```ruby
42
+ RedisObject.configure_store({:db: 2})
43
+ ```
44
+
45
+ The default storage adapter is `Redis`. The above config will connect to Redis on localhost on the default port (6379), but will `select` database number 2.
46
+
47
+ Or, you can configure multiple stores to use within an app by passing a second parameter to name the store (default is 'general')
48
+
49
+ ```ruby
50
+ RedisObject.configure_store({adapter: "Redis", :db: 4, :path: "/var/run/redis.sock"}, :message_queue)
51
+
52
+ class Message < RedisObject
53
+ use_store :message_queue
54
+ end
55
+ ```
56
+
57
+ ## 'Collections'
58
+ Object relationships are stored in collections of objects attached to other objects. To 'collect' an object onto another, you simply call `reference` to reference the objects (also aliased to the concat operator `<<`).
59
+
60
+ Collections are automatically created, and can be access by their plural, lower-case name to gather all of the items in a collection (returns an Enumerable `Collection` object), or by its singular lower-case name to just get one somewhat randomly (useful for 1 -> 1 style relationships).
61
+
62
+ Example:
63
+
64
+ ```ruby
65
+ class Person < RedisObject; end
66
+ class Address < RedisObject; end
67
+ john = Person.create("john")
68
+ john << Address.create({
69
+ :street => "123 Main St.",
70
+ :city => "San Francisco",
71
+ :state => "CA",
72
+ :zip => "12345"
73
+ })
74
+
75
+ john.addresses
76
+ # ["Address:john"]
77
+ john.address
78
+ # {
79
+ # :address_id => "john",
80
+ # :street => "123 Main St.",
81
+ # :city => "San Francisco",
82
+ # :state => "CA",
83
+ # :zip => "12345",
84
+ # :class=>"Address",
85
+ # :key=>"Address:john",
86
+ # :created_at=>Wed, 12 Dec 2012 16:49:26 -0800,
87
+ # :updated_at=>Wed, 12 Dec 2012 16:49:26 -0800
88
+ # }
89
+ ```
90
+
91
+ You may notice that the type of object, its basic storage key, and some timestamps are automatically created and updated appropriately.
92
+
93
+ It is important to note that collections inherit any indices of its underlying object type. See Indices below for examples.
94
+
95
+ ## Types
96
+ A few types of data can be specified for certain fields. The types supported are:
97
+
98
+ * Date
99
+ * Number
100
+ * Float
101
+ * Bool
102
+ * Array
103
+ * JSON (store any data that can be JSON-encoded - it will be automatically encoded/decoded when stored/accessed)
104
+
105
+ These types are also used for scoring when keeping field indices. If no type is specified, String is used, and no scoring is possible at this time.
106
+
107
+ Setting the type of a field is super easy:
108
+
109
+ ```ruby
110
+ class Person < RedisObject
111
+ bool :verified
112
+ json :meta
113
+ end
114
+
115
+ john = Person.create("john")
116
+ john.meta = {:external_id => "123456", :number => 123}
117
+ john.verified # false
118
+ ```
119
+
120
+ TODO: Add verified? and verified! -style methods automagically for boolean fields.
121
+
122
+ You can add your own custom types by defining filter methods for getting and setting a field, and can define a scoring function if you would like to index fields of your type.
123
+
124
+ Example:
125
+
126
+ ```ruby
127
+ class Person < RedisObject
128
+
129
+ def format_boolean(val)
130
+ val=="true"
131
+ end
132
+
133
+ def save_boolean(val)
134
+ val ? "true" : "false"
135
+ end
136
+
137
+ def score_boolean(val)
138
+ val ? 1 : 0
139
+ end
140
+
141
+ class << self
142
+ def bool(k)
143
+ field_formats[k] = :format_boolean
144
+ save_formats[k] = :save_boolean
145
+ score_formats[k] = :score_boolean
146
+ end
147
+ alias_method :boolean, :bool
148
+
149
+ end
150
+
151
+ end
152
+ ```
153
+
154
+ TODO: Make defining custom formats easier - no need to define class methods for this - could have helper function for it like `custom_format :bool, :get => :format_boolean` or similar.
155
+
156
+ ## Indices
157
+ Any field that can be scored can store a sidecar index by that score. These indices can be used to index items in a collection (internally, it is a simple Redis set intersection, so it is very fast). Timestamps are indexed by default for any object, so out of the box you can do:
158
+
159
+ ```ruby
160
+ Person.indexed(:created_at) # all Person objects, oldest first
161
+ Person.indexed(:created_at, 1, true) # newest Person (index_field, number of items, reverse sort?)
162
+ Person.latest # always available if timestamps are on - most recently created object of type
163
+ john.addresses.indexed(:created_at, 3, true) # john's 3 most recent addresses
164
+ Person.indexed(:updated_at, -1, true) do |person|
165
+ # iterate through Person objects in order of update times, most recent first
166
+ end
167
+ ```
168
+
169
+ Accessing indexed items always returns an Enumerator, so first/last/each/count/etc. are usable anywhere and will access objects only when iterated.
170
+
171
+ ## Named Views
172
+ `TODO: Add some damn View documentation.`
173
+
174
+ ## Links
175
+ Redis: [http://redis.io](http://redis.io)
176
+ RedisObject Code: [https://github.com/remotezygote/RedisObject](https://github.com/remotezygote/RedisObject)
177
+
178
+
179
+ [rubygems]: http://rubygems.org/gems/redis_object
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require 'bundler'
2
+ require 'rspec/core/rake_task'
3
+
4
+ task :default => :spec
5
+
6
+ RSpec::Core::RakeTask.new do |t|
7
+ t.pattern = './spec/*_spec.rb'
8
+ end
9
+
10
+ Bundler::GemHelper.install_tasks
@@ -0,0 +1,47 @@
1
+ require 'active_support/inflector'
2
+ require 'active_support/core_ext/date_time/conversions'
3
+ require 'yajl'
4
+
5
+ require "redis_object/storage"
6
+
7
+ require "redis_object/ext/script_cache"
8
+ require "redis_object/base"
9
+ require "redis_object/inheritance_tracking"
10
+ require "redis_object/storage"
11
+ require "redis_object/keys"
12
+ require "redis_object/types"
13
+ require "redis_object/defaults"
14
+ require "redis_object/collection"
15
+ require "redis_object/indices"
16
+ require "redis_object/timestamps"
17
+ require "redis_object/experimental/history"
18
+ require "redis_object/ext/views"
19
+ require "redis_object/ext/view_caching"
20
+ require "redis_object/ext/triggers"
21
+ require "redis_object/ext/filters"
22
+ require "redis_object/ext/benchmark"
23
+
24
+ module Seabright
25
+ class RedisObject
26
+
27
+ include Seabright::Filters
28
+ include Seabright::ObjectBase
29
+ include Seabright::InheritanceTracking
30
+ include Seabright::CachedScripts
31
+ include Seabright::Storage
32
+ include Seabright::Keys
33
+ include Seabright::Types
34
+ include Seabright::DefaultValues
35
+ include Seabright::Collections
36
+ include Seabright::Indices
37
+ include Seabright::Views
38
+ include Seabright::ViewCaching
39
+ include Seabright::Timestamps
40
+ include Seabright::History
41
+ include Seabright::Triggers
42
+ include Seabright::Benchmark
43
+
44
+ end
45
+ end
46
+
47
+ ::RedisObject = Seabright::RedisObject
@@ -0,0 +1,408 @@
1
+ module Seabright
2
+ module ObjectBase
3
+
4
+ def initialize(ident={})
5
+ if ident && (ident.class == String || (ident.class == Symbol && (ident = ident.to_s)))# && ident.gsub!(/.*:/,'') && ident.length > 0
6
+ load(ident.dup)
7
+ elsif ident && ident.class == Hash
8
+ ident[id_sym] ||= generate_id
9
+ if load(ident[id_sym])
10
+ mset(ident.dup)
11
+ end
12
+ end
13
+ self
14
+ end
15
+
16
+ def new_id(complexity = 8)
17
+ self.class.new_id(complexity)
18
+ end
19
+
20
+ def generate_id
21
+ self.class.generate_id
22
+ end
23
+
24
+ def reserve(k)
25
+ self.class.reserve(k)
26
+ end
27
+
28
+ def to_json
29
+ Yajl::Encoder.encode(actual)
30
+ end
31
+
32
+ def id
33
+ @id || get(id_sym) || set(id_sym, generate_id)
34
+ end
35
+
36
+ def load(o_id)
37
+ @id = o_id
38
+ true
39
+ end
40
+
41
+ def save
42
+ set(:class, self.class.name)
43
+ set(id_sym,id.gsub(/.*:/,''))
44
+ set(:key, key)
45
+ store.sadd(self.class.plname, key)
46
+ store.del(reserve_key)
47
+ end
48
+
49
+ def delete!
50
+ store.del key
51
+ store.del hkey
52
+ store.del reserve_key
53
+ store.srem(self.class.plname, key)
54
+ # store.smembers(ref_key).each do |k|
55
+ # if self.class.find_by_key(k)
56
+ #
57
+ # end
58
+ # end
59
+ dereference_all!
60
+ nil
61
+ end
62
+
63
+ def dereference_all!
64
+
65
+ end
66
+
67
+ def raw
68
+ store.hgetall(hkey).inject({}) {|acc,(k,v)| acc[k.to_sym] = enforce_format(k,v); acc }
69
+ end
70
+ alias_method :inspect, :raw
71
+ alias_method :actual, :raw
72
+
73
+ def get(k)
74
+ cached_hash_values[k.to_s] ||= Proc.new {|key|
75
+ if v = store.hget(hkey, key.to_s)
76
+ define_setter_getter(key)
77
+ end
78
+ v
79
+ }.call(k)
80
+ end
81
+
82
+ def [](k)
83
+ get(k)
84
+ end
85
+
86
+ def is_set?(k)
87
+ store.hexists(hkey, k.to_s)
88
+ end
89
+
90
+ def mset(dat)
91
+ store.hmset(hkey, *(dat.inject([]){|acc,(k,v)| acc + [k,v] }))
92
+ cached_hash_values.merge!(dat)
93
+ dat.each do |k,v|
94
+ define_setter_getter(k)
95
+ end
96
+ dat
97
+ end
98
+
99
+ def define_setter_getter(key)
100
+ define_access(key) do
101
+ get(key)
102
+ end
103
+ define_access("#{key.to_s}=") do |val|
104
+ set(key,val)
105
+ end
106
+ end
107
+
108
+ def undefine_setter_getter(key)
109
+ undefine_access(key)
110
+ undefine_access("#{key.to_s}=")
111
+ end
112
+
113
+ def set(k,v)
114
+ store.hset(hkey, k.to_s, v.to_s)
115
+ cached_hash_values[k.to_s] = v
116
+ define_setter_getter(k)
117
+ v
118
+ end
119
+
120
+ def setnx(k,v)
121
+ if success = store.hsetnx(hkey, k.to_s, v.to_s)
122
+ cached_hash_values[k.to_s] = v
123
+ define_setter_getter(k)
124
+ end
125
+ success
126
+ end
127
+
128
+ def []=(k,v)
129
+ set(k,v)
130
+ end
131
+
132
+ def unset(*k)
133
+ store.hdel(hkey, k.map(&:to_s))
134
+ k.each do |ky|
135
+ cached_hash_values.delete ky.to_s
136
+ undefine_setter_getter(ky)
137
+ end
138
+ end
139
+
140
+ private
141
+
142
+ SetPattern = /=$/.freeze
143
+
144
+ def method_missing(sym, *args, &block)
145
+ super if sym == :class
146
+ if sym.to_s =~ SetPattern
147
+ return super if args.size > 1
148
+ set(sym.to_s.gsub(SetPattern,'').to_sym,*args)
149
+ else
150
+ return super if !args.empty?
151
+ get(sym)
152
+ end
153
+ end
154
+
155
+ def id_sym(cls=self.class.cname)
156
+ "#{cls.split('::').last.downcase}_id".to_sym
157
+ end
158
+
159
+ def load_all_hash_values
160
+ @cached_hash_values = store.hgetall(hkey)
161
+ cached_hash_values.keys.dup.each do |key|
162
+ next if key == "class"
163
+ define_setter_getter(key)
164
+ end
165
+ end
166
+
167
+ def cached_hash_values
168
+ @cached_hash_values ||= {}
169
+ end
170
+
171
+ def define_access(key,&block)
172
+ return if self.respond_to?(key.to_sym)
173
+ metaclass = class << self; self; end
174
+ metaclass.send(:define_method, key.to_sym, &block)
175
+ end
176
+
177
+ def undefine_access(key)
178
+ return unless self.respond_to?(key.to_sym)
179
+ metaclass = class << self; self; end
180
+ metaclass.send(:remove_method, key.to_sym)
181
+ end
182
+
183
+ module ClassMethods
184
+
185
+ def generate_id
186
+ v = new_id
187
+ while exists?(v) do
188
+ puts "[RedisObject] Collision at id: #{v}" if Debug.verbose?
189
+ v = new_id
190
+ end
191
+ puts "[RedisObject] Reserving key: #{v}" if Debug.verbose?
192
+ reserve(v)
193
+ v
194
+ end
195
+
196
+ def reserve(k)
197
+ store.set(reserve_key(k),Time.now.to_s)
198
+ end
199
+
200
+ def new_id(complexity = 8)
201
+ rand(36**complexity).to_s(36)
202
+ end
203
+
204
+ def cname
205
+ self.name
206
+ end
207
+
208
+ def plname
209
+ cname.pluralize
210
+ end
211
+
212
+ def all
213
+ Enumerator.new do |y|
214
+ store.smembers(plname).each do |member|
215
+ if a = find_by_key(hkey(member))
216
+ y << a
217
+ else
218
+ puts "[#{name}] Object listed but not found: #{member}" if DEBUG
219
+ store.srem(plname,member)
220
+ end
221
+ end
222
+ end
223
+ end
224
+
225
+ def recollect!
226
+ store.keys("#{name}:*_h").each do |ky|
227
+ store.sadd(plname,ky.gsub(/_h$/,''))
228
+ end
229
+ end
230
+
231
+ def first
232
+ if m = store.smembers(plname)
233
+ self.grab(m.first)
234
+ else
235
+ nil
236
+ end
237
+ end
238
+
239
+ def each
240
+ all.each do |o|
241
+ yield o
242
+ end
243
+ end
244
+
245
+ RedisObject::ScriptSources::Matcher = "local itms = redis.call('SMEMBERS',KEYS[1])
246
+ local out = {}
247
+ local val
248
+ local pattern
249
+ for i, v in ipairs(itms) do
250
+ val = redis.call('HGET',v..'_h',ARGV[1])
251
+ if ARGV[2]:find('^pattern:') then
252
+ pattern = ARGV[2]:gsub('^pattern:','')
253
+ if val:match(pattern) ~= nil then
254
+ table.insert(out,itms[i])
255
+ end
256
+ else
257
+ if val == ARGV[2] then
258
+ table.insert(out,itms[i])
259
+ end
260
+ end
261
+ end
262
+ return out".gsub(/\t/,'').freeze
263
+
264
+ RedisObject::ScriptSources::MultiMatcher = "local itms = redis.call('SMEMBERS',KEYS[1])
265
+ local out = {}
266
+ local matchers = {}
267
+ local matcher = {}
268
+ local mod
269
+ for i=1,#ARGV do
270
+ mod = i % 2
271
+ if mod == 1 then
272
+ matcher[1] = ARGV[i]
273
+ else
274
+ matcher[2] = ARGV[i]
275
+ table.insert(matchers,matcher)
276
+ matcher = {}
277
+ end
278
+ end
279
+ local val
280
+ local good
281
+ local pattern
282
+ for i, v in ipairs(itms) do
283
+ good = true
284
+ for n=1,#matchers do
285
+ val = redis.call('HGET',v..'_h',matchers[n][1])
286
+ if val then
287
+ if matchers[n][2]:find('^pattern:') then
288
+ pattern = matchers[n][2]:gsub('^pattern:','')
289
+ if val:match(pattern) then
290
+ good = good
291
+ else
292
+ good = false
293
+ end
294
+ else
295
+ if val ~= matchers[n][2] then
296
+ good = false
297
+ end
298
+ end
299
+ else
300
+ good = false
301
+ end
302
+ end
303
+ if good == true then
304
+ table.insert(out,itms[i])
305
+ end
306
+ end
307
+ return out".gsub(/\t/,'').freeze
308
+
309
+ def match(pkt)
310
+ Enumerator.new do |y|
311
+ run_script(pkt.keys.count > 1 ? :MultiMatcher : :Matcher,[plname],pkt.flatten.map{|i| i.is_a?(Regexp) ? convert_regex_to_lua(i) : i.to_s }).each do |k|
312
+ y << find(k)
313
+ end
314
+ end
315
+ end
316
+
317
+ def convert_regex_to_lua(reg)
318
+ "pattern:#{reg.source.gsub("\\","")}"
319
+ end
320
+
321
+ def grab(ident)
322
+ case ident
323
+ when String, Symbol
324
+ return store.exists(self.hkey(ident.to_s)) ? self.new(ident.to_s) : nil
325
+ when Hash
326
+ return match(ident)
327
+ end
328
+ nil
329
+ end
330
+
331
+ def find(ident)
332
+ grab(ident)
333
+ end
334
+
335
+ def exists?(k)
336
+ store.exists(self.hkey(k)) || store.exists(self.reserve_key(k))
337
+ end
338
+
339
+ def create(ident={})
340
+ obj = new(ident)
341
+ obj.save
342
+ obj
343
+ end
344
+
345
+ # def dump
346
+ # out = []
347
+ # each do |obj|
348
+ # out << obj.dump
349
+ # end
350
+ # out.join("\n")
351
+ # end
352
+
353
+ def use_dbnum(db=0)
354
+ @dbnum = db
355
+ end
356
+
357
+ def dbnum
358
+ @dbnum ||= 0
359
+ end
360
+
361
+ def find_by_key(k)
362
+ if store.exists(k) && (cls = store.hget(k,:class))
363
+ return deep_const_get(cls.to_sym).new(store.hget(k,id_sym(cls)))
364
+ end
365
+ nil
366
+ end
367
+
368
+ def deep_const_get(const)
369
+ if Symbol === const
370
+ const = const.to_s
371
+ else
372
+ const = const.to_str.dup
373
+ end
374
+ if const.sub!(/^::/, '')
375
+ base = Object
376
+ else
377
+ base = self
378
+ end
379
+ const.split(/::/).inject(base) { |mod, name| mod.const_get(name) }
380
+ end
381
+
382
+ def save_all
383
+ all.each do |obj|
384
+ obj.save
385
+ end
386
+ true
387
+ end
388
+
389
+ def id_sym(cls=cname)
390
+ "#{cls.split('::').last.downcase}_id".to_sym
391
+ end
392
+
393
+ def describe
394
+ all_keys.inject({}) do |acc,(k,v)|
395
+ acc[k.to_sym] ||= [:string, 0]
396
+ acc[k.to_sym][1] += 1
397
+ acc
398
+ end
399
+ end
400
+
401
+ end
402
+
403
+ def self.included(base)
404
+ base.extend(ClassMethods)
405
+ end
406
+
407
+ end
408
+ end