ohm 0.0.32 → 0.0.33
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.
- data/README.markdown +71 -36
- data/lib/ohm/collection.rb +186 -0
- data/lib/ohm/compat-1.8.6.rb +9 -0
- data/lib/ohm/key.rb +46 -0
- data/lib/ohm/redis.rb +0 -1
- data/lib/ohm.rb +238 -202
- data/test/indices_test.rb +27 -21
- data/test/model_test.rb +181 -45
- data/test/redis_test.rb +1 -1
- metadata +4 -2
data/lib/ohm.rb
CHANGED
@@ -4,16 +4,22 @@ require "base64"
|
|
4
4
|
require File.join(File.dirname(__FILE__), "ohm", "redis")
|
5
5
|
require File.join(File.dirname(__FILE__), "ohm", "validations")
|
6
6
|
require File.join(File.dirname(__FILE__), "ohm", "compat-1.8.6")
|
7
|
+
require File.join(File.dirname(__FILE__), "ohm", "key")
|
8
|
+
require File.join(File.dirname(__FILE__), "ohm", "collection")
|
7
9
|
|
8
10
|
module Ohm
|
9
11
|
|
10
12
|
# Provides access to the Redis database. This is shared accross all models and instances.
|
11
13
|
def redis
|
12
|
-
|
14
|
+
threaded[:redis] ||= connection(*options)
|
13
15
|
end
|
14
16
|
|
15
17
|
def redis=(connection)
|
16
|
-
|
18
|
+
threaded[:redis] = connection
|
19
|
+
end
|
20
|
+
|
21
|
+
def threaded
|
22
|
+
Thread.current[:ohm] ||= {}
|
17
23
|
end
|
18
24
|
|
19
25
|
# Connect to a redis database.
|
@@ -30,8 +36,15 @@ module Ohm
|
|
30
36
|
@options = options
|
31
37
|
end
|
32
38
|
|
39
|
+
# Return a connection to Redis.
|
40
|
+
#
|
41
|
+
# This is a wapper around Ohm::Redis.new(options)
|
42
|
+
def connection(*options)
|
43
|
+
Ohm::Redis.new(*options)
|
44
|
+
end
|
45
|
+
|
33
46
|
def options
|
34
|
-
@options
|
47
|
+
@options || []
|
35
48
|
end
|
36
49
|
|
37
50
|
# Clear the database.
|
@@ -41,54 +54,55 @@ module Ohm
|
|
41
54
|
|
42
55
|
# Join the parameters with ":" to create a key.
|
43
56
|
def key(*args)
|
44
|
-
args
|
57
|
+
Key[*args]
|
45
58
|
end
|
46
59
|
|
47
|
-
module_function :key, :connect, :flush, :redis, :redis=, :options
|
60
|
+
module_function :key, :connect, :connection, :flush, :redis, :redis=, :options, :threaded
|
48
61
|
|
49
|
-
|
62
|
+
Error = Class.new(StandardError)
|
63
|
+
|
64
|
+
class Model
|
50
65
|
class Collection
|
51
66
|
include Enumerable
|
52
67
|
|
53
|
-
|
68
|
+
attr :raw
|
69
|
+
attr :model
|
70
|
+
|
71
|
+
def initialize(key, model, db = model.db)
|
72
|
+
@raw = self.class::Raw.new(key, db)
|
73
|
+
@model = model
|
74
|
+
end
|
54
75
|
|
55
|
-
def
|
56
|
-
|
57
|
-
self.key = key
|
58
|
-
self.model = model
|
76
|
+
def <<(model)
|
77
|
+
raw << model.id
|
59
78
|
end
|
60
79
|
|
80
|
+
alias add <<
|
81
|
+
|
61
82
|
def each(&block)
|
62
|
-
|
83
|
+
raw.each do |id|
|
84
|
+
block.call(model[id])
|
85
|
+
end
|
63
86
|
end
|
64
87
|
|
65
|
-
|
66
|
-
|
67
|
-
instantiate(raw)
|
88
|
+
def key
|
89
|
+
raw.key
|
68
90
|
end
|
69
91
|
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
#
|
85
|
-
# @blog.posts.sort(:by => :votes, :start => 5, :limit => 10")
|
86
|
-
def sort(options = {})
|
87
|
-
return [] if empty?
|
88
|
-
options[:start] ||= 0
|
89
|
-
options[:limit] = [options[:start], options[:limit]] if options[:limit]
|
90
|
-
result = db.sort(key, options)
|
91
|
-
options[:get] ? result : instantiate(result)
|
92
|
+
def first(options = {})
|
93
|
+
if options[:by]
|
94
|
+
sort_by(options.delete(:by), options.merge(:limit => 1)).first
|
95
|
+
else
|
96
|
+
model[raw.first(options)]
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def [](index)
|
101
|
+
model[raw[index]]
|
102
|
+
end
|
103
|
+
|
104
|
+
def sort(*args)
|
105
|
+
raw.sort(*args).map(&model)
|
92
106
|
end
|
93
107
|
|
94
108
|
# Sort the model instances by the given attribute.
|
@@ -102,163 +116,58 @@ module Ohm
|
|
102
116
|
# user.name == "A"
|
103
117
|
# # => true
|
104
118
|
def sort_by(att, options = {})
|
105
|
-
|
106
|
-
end
|
107
|
-
|
108
|
-
# Sort the model instances by id and return the first instance
|
109
|
-
# found. If a :by option is provided with a valid attribute name, the
|
110
|
-
# method sort_by is used instead and the option provided is passed as the
|
111
|
-
# first parameter.
|
112
|
-
#
|
113
|
-
# @see #sort
|
114
|
-
# @see #sort_by
|
115
|
-
# @return [Ohm::Model, nil] Returns the first instance found or nil.
|
116
|
-
def first(options = {})
|
117
|
-
options = options.merge(:limit => 1)
|
118
|
-
options[:by] ?
|
119
|
-
sort_by(options.delete(:by), options).first :
|
120
|
-
sort(options).first
|
121
|
-
end
|
119
|
+
options.merge!(:by => model.key("*", att))
|
122
120
|
|
123
|
-
|
124
|
-
|
121
|
+
if options[:get]
|
122
|
+
raw.sort(options.merge(:get => model.key("*", options[:get])))
|
123
|
+
else
|
124
|
+
sort(options)
|
125
|
+
end
|
125
126
|
end
|
126
127
|
|
127
|
-
def
|
128
|
-
|
128
|
+
def delete(model)
|
129
|
+
raw.delete(model.id)
|
130
|
+
model
|
129
131
|
end
|
130
132
|
|
131
|
-
# @return [true, false] Returns whether or not the collection is empty.
|
132
|
-
def empty?
|
133
|
-
size.zero?
|
134
|
-
end
|
135
|
-
|
136
|
-
# Clears the values in the collection.
|
137
133
|
def clear
|
138
|
-
|
139
|
-
self
|
134
|
+
raw.clear
|
140
135
|
end
|
141
136
|
|
142
|
-
|
143
|
-
|
144
|
-
values.each { |value| self << value }
|
137
|
+
def concat(models)
|
138
|
+
raw.concat(models.map { |model| model.id })
|
145
139
|
self
|
146
140
|
end
|
147
141
|
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
concat(values)
|
152
|
-
end
|
153
|
-
|
154
|
-
# @param value [Ohm::Model#id] Adds the id of the object if it's an Ohm::Model.
|
155
|
-
def add(model)
|
156
|
-
self << model.id
|
157
|
-
end
|
158
|
-
|
159
|
-
private
|
160
|
-
|
161
|
-
def instantiate(raw)
|
162
|
-
model ? raw.collect { |id| model[id] } : raw
|
163
|
-
end
|
164
|
-
end
|
165
|
-
|
166
|
-
# Represents a Redis list.
|
167
|
-
#
|
168
|
-
# @example Use a list attribute.
|
169
|
-
#
|
170
|
-
# class Event < Ohm::Model
|
171
|
-
# attribute :name
|
172
|
-
# list :participants
|
173
|
-
# end
|
174
|
-
#
|
175
|
-
# event = Event.create :name => "Redis Meeting"
|
176
|
-
# event.participants << "Albert"
|
177
|
-
# event.participants << "Benoit"
|
178
|
-
# event.participants.all #=> ["Albert", "Benoit"]
|
179
|
-
class List < Collection
|
180
|
-
|
181
|
-
# @param value [#to_s] Pushes value to the tail of the list.
|
182
|
-
def << value
|
183
|
-
db.rpush(key, value)
|
184
|
-
end
|
185
|
-
|
186
|
-
alias push <<
|
187
|
-
|
188
|
-
# @return [String] Return and remove the last element of the list.
|
189
|
-
def pop
|
190
|
-
db.rpop(key)
|
191
|
-
end
|
192
|
-
|
193
|
-
# @return [String] Return and remove the first element of the list.
|
194
|
-
def shift
|
195
|
-
db.lpop(key)
|
142
|
+
def replace(models)
|
143
|
+
raw.replace(models.map { |model| model.id })
|
144
|
+
self
|
196
145
|
end
|
197
146
|
|
198
|
-
|
199
|
-
|
200
|
-
db.lpush(key, value)
|
147
|
+
def include?(model)
|
148
|
+
raw.include?(model.id)
|
201
149
|
end
|
202
150
|
|
203
|
-
|
204
|
-
|
205
|
-
db.list(key)
|
151
|
+
def empty?
|
152
|
+
raw.empty?
|
206
153
|
end
|
207
154
|
|
208
|
-
# @return [Integer] Returns the number of elements in the list.
|
209
155
|
def size
|
210
|
-
|
156
|
+
raw.size
|
211
157
|
end
|
212
158
|
|
213
|
-
def
|
214
|
-
raw.
|
159
|
+
def all
|
160
|
+
raw.to_a.map(&model)
|
215
161
|
end
|
216
162
|
|
217
|
-
|
218
|
-
"#<List: #{raw.inspect}>"
|
219
|
-
end
|
163
|
+
alias to_a all
|
220
164
|
end
|
221
165
|
|
222
|
-
# Represents a Redis set.
|
223
|
-
#
|
224
|
-
# @example Use a set attribute.
|
225
|
-
#
|
226
|
-
# class Company < Ohm::Model
|
227
|
-
# attribute :name
|
228
|
-
# set :employees
|
229
|
-
# end
|
230
|
-
#
|
231
|
-
# company = Company.create :name => "Redis Co."
|
232
|
-
# company.employees << "Albert"
|
233
|
-
# company.employees << "Benoit"
|
234
|
-
# company.employees.all #=> ["Albert", "Benoit"]
|
235
|
-
# company.include?("Albert") #=> true
|
236
166
|
class Set < Collection
|
237
|
-
|
238
|
-
# @param value [#to_s] Adds value to the list.
|
239
|
-
def << value
|
240
|
-
db.sadd(key, value)
|
241
|
-
end
|
242
|
-
|
243
|
-
def delete(value)
|
244
|
-
db.srem(key, value)
|
245
|
-
end
|
246
|
-
|
247
|
-
def include?(value)
|
248
|
-
db.sismember(key, value)
|
249
|
-
end
|
250
|
-
|
251
|
-
def raw
|
252
|
-
db.smembers(key)
|
253
|
-
end
|
254
|
-
|
255
|
-
# @return [Integer] Returns the number of elements in the set.
|
256
|
-
def size
|
257
|
-
db.scard(key)
|
258
|
-
end
|
167
|
+
Raw = Ohm::Set
|
259
168
|
|
260
169
|
def inspect
|
261
|
-
"#<Set: #{
|
170
|
+
"#<Set (#{model}): #{all.inspect}>"
|
262
171
|
end
|
263
172
|
|
264
173
|
# Returns an intersection with the sets generated from the passed hash.
|
@@ -285,36 +194,42 @@ module Ohm
|
|
285
194
|
|
286
195
|
# Apply a redis operation on a collection of sets.
|
287
196
|
def apply(operation, hash, glue)
|
288
|
-
|
289
|
-
target
|
290
|
-
|
291
|
-
self.class.new(db, target, model)
|
197
|
+
target = key.volatile.group(glue).append(*keys(hash))
|
198
|
+
model.db.send(operation, target, *target.parts)
|
199
|
+
Set.new(target, model)
|
292
200
|
end
|
293
201
|
|
294
202
|
# Transform a hash of attribute/values into an array of keys.
|
295
203
|
def keys(hash)
|
296
|
-
|
297
|
-
|
298
|
-
|
204
|
+
[].tap do |keys|
|
205
|
+
hash.each do |key, values|
|
206
|
+
values = [values] unless values.kind_of?(Array) # Yes, Array() is different in 1.8.x.
|
207
|
+
values.each do |v|
|
208
|
+
keys << model.index_key_for(key, v)
|
209
|
+
end
|
299
210
|
end
|
300
211
|
end
|
301
212
|
end
|
302
213
|
end
|
303
214
|
|
304
|
-
class
|
215
|
+
class List < Collection
|
216
|
+
Raw = Ohm::List
|
217
|
+
|
305
218
|
def inspect
|
306
|
-
"#<
|
219
|
+
"#<List (#{model}): #{all.inspect}>"
|
307
220
|
end
|
221
|
+
end
|
308
222
|
|
309
|
-
|
310
|
-
|
223
|
+
class Index < Set
|
224
|
+
def apply(operation, hash, glue)
|
225
|
+
if hash.keys.size == 1
|
226
|
+
return Set.new(keys(hash).first, model)
|
227
|
+
else
|
228
|
+
super
|
229
|
+
end
|
311
230
|
end
|
312
231
|
end
|
313
|
-
end
|
314
232
|
|
315
|
-
Error = Class.new(StandardError)
|
316
|
-
|
317
|
-
class Model
|
318
233
|
module Validations
|
319
234
|
include Ohm::Validations
|
320
235
|
|
@@ -339,12 +254,6 @@ module Ohm
|
|
339
254
|
end
|
340
255
|
end
|
341
256
|
|
342
|
-
class CannotDeleteIndex < Error
|
343
|
-
def message
|
344
|
-
"You tried to delete an internal index used by Ohm."
|
345
|
-
end
|
346
|
-
end
|
347
|
-
|
348
257
|
class IndexNotFound < Error
|
349
258
|
def initialize(att)
|
350
259
|
@att = att
|
@@ -399,7 +308,7 @@ module Ohm
|
|
399
308
|
#
|
400
309
|
# @param name [Symbol] Name of the list.
|
401
310
|
def self.list(name, model = nil)
|
402
|
-
|
311
|
+
attr_collection_reader(name, :List, model)
|
403
312
|
collections << name unless collections.include?(name)
|
404
313
|
end
|
405
314
|
|
@@ -409,7 +318,7 @@ module Ohm
|
|
409
318
|
#
|
410
319
|
# @param name [Symbol] Name of the set.
|
411
320
|
def self.set(name, model = nil)
|
412
|
-
|
321
|
+
attr_collection_reader(name, :Set, model)
|
413
322
|
collections << name unless collections.include?(name)
|
414
323
|
end
|
415
324
|
|
@@ -432,17 +341,118 @@ module Ohm
|
|
432
341
|
indices << att unless indices.include?(att)
|
433
342
|
end
|
434
343
|
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
344
|
+
# Define a reference to another object.
|
345
|
+
#
|
346
|
+
# @example
|
347
|
+
# class Comment < Ohm::Model
|
348
|
+
# attribute :content
|
349
|
+
# reference :post, Post
|
350
|
+
# end
|
351
|
+
#
|
352
|
+
# @post = Post.create :content => "Interesting stuff"
|
353
|
+
#
|
354
|
+
# @comment = Comment.create(:content => "Indeed!", :post => @post)
|
355
|
+
#
|
356
|
+
# @comment.post.content
|
357
|
+
# # => "Interesting stuff"
|
358
|
+
#
|
359
|
+
# @comment.post = Post.create(:content => "Wonderful stuff")
|
360
|
+
#
|
361
|
+
# @comment.post.content
|
362
|
+
# # => "Wonderful stuff"
|
363
|
+
#
|
364
|
+
# @comment.post.update(:content => "Magnific stuff")
|
365
|
+
#
|
366
|
+
# @comment.post.content
|
367
|
+
# # => "Magnific stuff"
|
368
|
+
#
|
369
|
+
# @comment.post = nil
|
370
|
+
#
|
371
|
+
# @comment.post
|
372
|
+
# # => nil
|
373
|
+
#
|
374
|
+
# @see Ohm::Model::collection
|
375
|
+
def self.reference(name, model)
|
376
|
+
reader = :"#{name}_id"
|
377
|
+
writer = :"#{name}_id="
|
378
|
+
|
379
|
+
attribute reader
|
380
|
+
index reader
|
381
|
+
|
382
|
+
define_memoized_method(name) do
|
383
|
+
model[send(reader)]
|
384
|
+
end
|
385
|
+
|
386
|
+
define_method(:"#{name}=") do |value|
|
387
|
+
instance_variable_set("@#{name}", nil)
|
388
|
+
send(writer, value ? value.id : nil)
|
389
|
+
end
|
390
|
+
|
391
|
+
define_method(writer) do |value|
|
392
|
+
instance_variable_set("@#{name}", nil)
|
393
|
+
write_local(reader, value)
|
394
|
+
end
|
395
|
+
end
|
396
|
+
|
397
|
+
# Define a collection of objects which have a {Ohm::Model::reference reference}
|
398
|
+
# to this model.
|
399
|
+
#
|
400
|
+
# class Comment < Ohm::Model
|
401
|
+
# attribute :content
|
402
|
+
# reference :post, Post
|
403
|
+
# end
|
404
|
+
#
|
405
|
+
# class Post < Ohm::Model
|
406
|
+
# attribute :content
|
407
|
+
# collection :comments, Comment
|
408
|
+
# reference :author, Person
|
409
|
+
# end
|
410
|
+
#
|
411
|
+
# class Person < Ohm::Model
|
412
|
+
# attribute :name
|
413
|
+
#
|
414
|
+
# # When the name of the reference cannot be inferred,
|
415
|
+
# # you need to specify it in the third param.
|
416
|
+
# collection :posts, Post, :author
|
417
|
+
# end
|
418
|
+
#
|
419
|
+
# @person = Person.create :name => "Albert"
|
420
|
+
# @post = Post.create :content => "Interesting stuff", :author => @person
|
421
|
+
# @comment = Comment.create :content => "Indeed!", :post => @post
|
422
|
+
#
|
423
|
+
# @post.comments.first.content
|
424
|
+
# # => "Indeed!"
|
425
|
+
#
|
426
|
+
# @post.author.name
|
427
|
+
# # => "Albert"
|
428
|
+
#
|
429
|
+
# *Important*: please note that even though a collection is a {Ohm::Set Set},
|
430
|
+
# you should not add or remove objects from this collection directly.
|
431
|
+
#
|
432
|
+
# @see Ohm::Model::reference
|
433
|
+
# @param name [Symbol] Name of the collection.
|
434
|
+
# @param model [Constant] Model where the reference is defined.
|
435
|
+
# @param reference [Symbol] Reference as defined in the associated model.
|
436
|
+
def self.collection(name, model, reference = to_reference)
|
437
|
+
define_method(name) { model.find(:"#{reference}_id" => send(:id)) }
|
438
|
+
end
|
439
|
+
|
440
|
+
def self.to_reference
|
441
|
+
name.to_s.gsub(/([a-z\d])([A-Z])/, '\1_\2').downcase.to_sym
|
442
|
+
end
|
443
|
+
|
444
|
+
def self.attr_collection_reader(name, type, model)
|
445
|
+
if model
|
446
|
+
define_memoized_method(name) { Ohm::Model::const_get(type).new(key(name), model, db) }
|
447
|
+
else
|
448
|
+
define_memoized_method(name) { Ohm::const_get(type).new(key(name), db) }
|
439
449
|
end
|
440
450
|
end
|
441
451
|
|
442
|
-
def self.
|
452
|
+
def self.define_memoized_method(name, &block)
|
443
453
|
define_method(name) do
|
444
454
|
instance_variable_get("@#{name}") ||
|
445
|
-
instance_variable_set("@#{name}",
|
455
|
+
instance_variable_set("@#{name}", instance_eval(&block))
|
446
456
|
end
|
447
457
|
end
|
448
458
|
|
@@ -455,7 +465,7 @@ module Ohm
|
|
455
465
|
end
|
456
466
|
|
457
467
|
def self.all
|
458
|
-
@all ||=
|
468
|
+
@all ||= Ohm::Model::Index.new(key(:all), self)
|
459
469
|
end
|
460
470
|
|
461
471
|
def self.attributes
|
@@ -588,8 +598,9 @@ module Ohm
|
|
588
598
|
def mutex
|
589
599
|
lock!
|
590
600
|
yield
|
591
|
-
unlock!
|
592
601
|
self
|
602
|
+
ensure
|
603
|
+
unlock!
|
593
604
|
end
|
594
605
|
|
595
606
|
def inspect
|
@@ -606,6 +617,25 @@ module Ohm
|
|
606
617
|
"#<#{self.class}:#{new? ? "?" : id} #{everything.map {|e| e.join("=") }.join(" ")}>"
|
607
618
|
end
|
608
619
|
|
620
|
+
# Makes the model connect to a different Redis instance.
|
621
|
+
#
|
622
|
+
# @example
|
623
|
+
#
|
624
|
+
# class Post < Ohm::Model
|
625
|
+
# connect :port => 6380, :db => 2
|
626
|
+
#
|
627
|
+
# attribute :body
|
628
|
+
# end
|
629
|
+
#
|
630
|
+
# # Since these settings are usually environment-specific,
|
631
|
+
# # you may want to call this method from outside of the class
|
632
|
+
# # definition:
|
633
|
+
# Post.connect(:port => 6380, :db => 2)
|
634
|
+
#
|
635
|
+
def self.connect(*options)
|
636
|
+
self.db = Ohm.connection(*options)
|
637
|
+
end
|
638
|
+
|
609
639
|
protected
|
610
640
|
|
611
641
|
def key(*args)
|
@@ -623,7 +653,8 @@ module Ohm
|
|
623
653
|
# This method will be removed once MSET becomes standard.
|
624
654
|
def write_with_set
|
625
655
|
attributes.each do |att|
|
626
|
-
|
656
|
+
value = send(att)
|
657
|
+
value.to_s.empty? ?
|
627
658
|
db.set(key(att), value) :
|
628
659
|
db.del(key(att))
|
629
660
|
end
|
@@ -634,7 +665,7 @@ module Ohm
|
|
634
665
|
# available once MSET becomes standard.
|
635
666
|
def write_with_mset
|
636
667
|
unless attributes.empty?
|
637
|
-
rems, adds = attributes.map { |a| [key(a), send(a)] }.partition { |t| t.last.
|
668
|
+
rems, adds = attributes.map { |a| [key(a), send(a)] }.partition { |t| t.last.to_s.empty? }
|
638
669
|
db.del(*rems.flatten.compact) unless rems.empty?
|
639
670
|
db.mset(adds.flatten) unless adds.empty?
|
640
671
|
end
|
@@ -642,8 +673,13 @@ module Ohm
|
|
642
673
|
|
643
674
|
private
|
644
675
|
|
676
|
+
# Provides access to the Redis database. This is shared accross all models and instances.
|
645
677
|
def self.db
|
646
|
-
Ohm.redis
|
678
|
+
Ohm.threaded[self] || Ohm.redis
|
679
|
+
end
|
680
|
+
|
681
|
+
def self.db=(connection)
|
682
|
+
Ohm.threaded[self] = connection
|
647
683
|
end
|
648
684
|
|
649
685
|
def self.key(*args)
|
@@ -659,7 +695,7 @@ module Ohm
|
|
659
695
|
end
|
660
696
|
|
661
697
|
def db
|
662
|
-
|
698
|
+
self.class.db
|
663
699
|
end
|
664
700
|
|
665
701
|
def delete_attributes(atts)
|