redis_object 0.5.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.
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