ohm 0.1.5 → 1.0.0.alpha1

Sign up to get free protection for your applications and to get access to all the features.
data/Rakefile CHANGED
@@ -27,7 +27,7 @@ end
27
27
  task :test do
28
28
  require File.expand_path("./test/helper", File.dirname(__FILE__))
29
29
 
30
- Cutest.run(Dir["test/*_test.rb"])
30
+ Cutest.run(Dir["test/*.rb"])
31
31
  end
32
32
 
33
33
  desc "Generate documentation"
@@ -90,3 +90,21 @@ namespace :examples do
90
90
  end
91
91
  end
92
92
 
93
+ task :paste_lua_inline do
94
+ def wrap(const, data)
95
+ ret = "#{const} = (<<-EOT).gsub(/^ {4}/, "")\n"
96
+ ret << data.gsub(/^/, " ")
97
+ ret << " EOT\n"
98
+ end
99
+
100
+ save = File.read("./lua/save.lua")
101
+ del = File.read("./lua/delete.lua")
102
+ # save = "foo"
103
+ # del = "bar"
104
+
105
+ ohm = File.read("./lib/ohm.rb", encoding: "utf-8")
106
+ ohm.gsub!(/SAVE =(.*?)$(.*?)EOT/m, wrap("SAVE", save))
107
+ ohm.gsub!(/DELETE =(.*?)$(.*?)EOT/m, wrap("DELETE", del))
108
+
109
+ puts ohm
110
+ end
data/lib/ohm.rb CHANGED
@@ -1,2000 +1,1231 @@
1
1
  # encoding: UTF-8
2
2
 
3
- require "base64"
4
- require "redis"
5
3
  require "nest"
6
-
7
- require File.join(File.dirname(__FILE__), "ohm", "pattern")
8
- require File.join(File.dirname(__FILE__), "ohm", "validations")
9
- require File.join(File.dirname(__FILE__), "ohm", "compat-1.8.6")
10
- require File.join(File.dirname(__FILE__), "ohm", "key")
4
+ require "redis"
5
+ require "securerandom"
6
+ require "scrivener"
7
+ require "ohm/transaction"
11
8
 
12
9
  module Ohm
13
10
 
14
- # Provides access to the _Redis_ database. It is highly recommended that you
15
- # use this sparingly, and only if you really know what you're doing.
11
+ # All of the known errors in Ohm can be traced back to one of these
12
+ # exceptions.
16
13
  #
17
- # The better way to access the _Redis_ database and do raw _Redis_
18
- # commands would be one of the following:
14
+ # MissingID:
19
15
  #
20
- # 1. Use {Ohm::Model.key} or {Ohm::Model#key}. So if the name of your
21
- # model is *Post*, it would be *Post.key* or the protected method
22
- # *#key* which should be used within your *Post* model.
16
+ # Comment.new.id # => Error
17
+ # Comment.new.key # => Error
23
18
  #
24
- # 2. Use {Ohm::Model.db} or {Ohm::Model#db}. Although this is also
25
- # accessible, it is much cleaner and terse to use {Ohm::Model.key}.
19
+ # Solution: you need to save your model first.
26
20
  #
27
- # @example
21
+ # IndexNotFound:
28
22
  #
29
- # class Post < Ohm::Model
30
- # def comment_ids
31
- # key[:comments].zrange(0, -1)
32
- # end
23
+ # Comment.find(foo: "Bar") # => Error
33
24
  #
34
- # def add_comment_id(id)
35
- # key[:comments].zadd(Time.now.to_i, id)
36
- # end
25
+ # Solution: add an index with `Comment.index :foo`.
37
26
  #
38
- # def remove_comment_id(id)
39
- # # Let's use the db style here just to demonstrate.
40
- # db.zrem key[:comments], id
41
- # end
42
- # end
27
+ # UniqueIndexViolation:
43
28
  #
44
- # Post.key[:latest].sadd(1)
45
- # Post.key[:latest].smembers == ["1"]
46
- # # => true
29
+ # Raised when trying to save an object with a `unique` index for
30
+ # which the value already exists.
47
31
  #
48
- # Post.key[:latest] == "Post:latest"
49
- # # => true
32
+ # Solution: rescue `Ohm::UniqueIndexViolation` during save, but
33
+ # also, do some validations even before attempting to save.
50
34
  #
51
- # p = Post.create
52
- # p.comment_ids == []
53
- # # => true
54
- #
55
- # p.add_comment_id(101)
56
- # p.comment_ids == ["101"]
57
- # # => true
58
- #
59
- # p.remove_comment_id(101)
60
- # p.comment_ids == []
61
- # # => true
62
- def self.redis
63
- threaded[:redis] ||= connection(*options)
35
+ class Error < StandardError; end
36
+ class MissingID < Error; end
37
+ class IndexNotFound < Error; end
38
+ class UniqueIndexViolation < Error; end
39
+
40
+ # Instead of monkey patching Kernel or trying to be clever, it's
41
+ # best to confine all the helper methods in a Utils module.
42
+ module Utils
43
+
44
+ # Used by: `attribute`, `counter`, `set`, `reference`,
45
+ # `collection`.
46
+ #
47
+ # Employed as a solution to avoid `NameError` problems when trying
48
+ # to load models referring to other models not yet loaded.
49
+ #
50
+ # Example:
51
+ #
52
+ # class Comment < Ohm::Model
53
+ # reference :user, User # NameError undefined constant User.
54
+ # end
55
+ #
56
+ # Instead of relying on some clever `const_missing` hack, we can
57
+ # simply use a Symbol.
58
+ #
59
+ # class Comment < Ohm::Model
60
+ # reference :user, :User
61
+ # end
62
+ #
63
+ def self.const(context, name)
64
+ case name
65
+ when Symbol then context.const_get(name)
66
+ else name
67
+ end
68
+ end
64
69
  end
65
70
 
66
- # Assign a new _Redis_ connection. Internally used by {Ohm.connect}
67
- # to clear the cached _Redis_ instance.
68
- #
69
- # If you're looking to change the connection or reconnect with different
70
- # parameters, try {Ohm.connect} or {Ohm::Model.connect}.
71
- # @see connect
72
- # @see Model.connect
73
- # @param connection [Redis] an instance created using `Redis.new`.
74
- def self.redis=(connection)
75
- threaded[:redis] = connection
71
+ class Connection
72
+ attr_accessor :context
73
+ attr_accessor :options
74
+
75
+ def initialize(context = :main, options = {})
76
+ @context = context
77
+ @options = options
78
+ end
79
+
80
+ def reset!
81
+ threaded[context] = nil
82
+ end
83
+
84
+ def start(options = {})
85
+ self.options = options
86
+ self.reset!
87
+ end
88
+
89
+ def redis
90
+ threaded[context] ||= Redis.connect(options)
91
+ end
92
+
93
+ def threaded
94
+ Thread.current[:ohm] ||= {}
95
+ end
76
96
  end
77
97
 
78
- # @private Used internally by Ohm for thread safety.
79
- def self.threaded
80
- Thread.current[:ohm] ||= {}
98
+ def self.conn
99
+ @conn ||= Connection.new
81
100
  end
82
101
 
83
- # Connect to a _Redis_ database.
84
- #
85
- # It is also worth mentioning that you can pass in a *URI* e.g.
86
- #
87
- # Ohm.connect :url => "redis://127.0.0.1:6379/0"
88
- #
89
- # Note that the value *0* refers to the database number for the given
90
- # _Redis_ instance.
102
+ # Stores the connection options for the Redis instance.
91
103
  #
92
- # Also you can use {Ohm.connect} without any arguments. The behavior will
93
- # be as follows:
104
+ # Examples:
94
105
  #
95
- # # Connect to redis://127.0.0.1:6379/0
96
- # Ohm.connect
106
+ # Ohm.connect(port: 6380, db: 1, host: "10.0.1.1")
107
+ # Ohm.connect(url: "redis://10.0.1.1:6380/1")
97
108
  #
98
- # # Connect to redis://10.0.0.100:22222/5
99
- # ENV["REDIS_URL"] = "redis://10.0.0.100:22222/5"
100
- # Ohm.connect
109
+ # All of the options are simply passed on to `Redis.connect`.
101
110
  #
102
- # @param options [{Symbol => #to_s}] An options hash.
103
- # @see file:README.html#connecting Ohm.connect options documentation.
104
- #
105
- # @example Connect to a database in port 6380.
106
- # Ohm.connect(:port => 6380)
107
- def self.connect(*options)
108
- self.redis = nil
109
- @options = options
111
+ def self.connect(options = {})
112
+ conn.start(options)
110
113
  end
111
114
 
112
- # @private Return a connection to Redis.
115
+ # Use this if you want to do quick ad hoc redis commands against the
116
+ # defined Ohm connection.
113
117
  #
114
- # This is a wrapper around Redis.connect(options)
115
- def self.connection(*options)
116
- Redis.connect(*options)
117
- end
118
-
119
- # @private Stores the connection options for Ohm.redis.
120
- def self.options
121
- @options = [] unless defined? @options
122
- @options
118
+ # Examples:
119
+ #
120
+ # Ohm.redis.keys("User:*")
121
+ # Ohm.redis.set("foo", "bar")
122
+ #
123
+ def self.redis
124
+ conn.redis
123
125
  end
124
126
 
125
- # Clear the database. You typically use this only during testing,
126
- # or when you seed your site.
127
- #
128
- # @see http://code.google.com/p/redis/wiki/FlushdbCommand FLUSHDB in the
129
- # Redis Command Reference.
127
+ # Wrapper for Ohm.redis.flushdb.
130
128
  def self.flush
131
129
  redis.flushdb
132
130
  end
133
131
 
134
- # The base class of all *Ohm* errors. Can be used as a catch all for
135
- # Ohm related errors.
136
- class Error < StandardError; end
137
-
138
- # This is the class that you need to extend in order to define your
139
- # own models.
140
- #
141
- # Probably the most magic happening within {Ohm::Model} is the catching
142
- # of {Ohm::Model.const_missing} exceptions to allow the use of constants
143
- # even before they are defined.
144
- #
145
- # @example
146
- #
147
- # class Post < Ohm::Model
148
- # reference :author, User # no User definition yet!
149
- # end
150
- #
151
- # class User < Ohm::Model
152
- # end
153
- #
154
- # @see Model.const_missing
155
- class Model
156
-
157
- # Wraps a model name for lazy evaluation.
158
- class Wrapper < BasicObject
159
-
160
- # Allows you to use a constant even before it is defined. This solves
161
- # the issue of having to require inter-project dependencies in a very
162
- # simple and "magic-free" manner.
163
- #
164
- # Example of how it was done before Wrapper existed:
165
- #
166
- # require "./app/models/user"
167
- # require "./app/models/comment"
168
- #
169
- # class Post < Ohm::Model
170
- # reference :author, User
171
- # list :comments, Comment
172
- # end
173
- #
174
- # Now, you can simply do the following:
175
- # class Post < Ohm::Model
176
- # reference :author, User
177
- # list :comments, Comment
178
- # end
179
- #
180
- # @example
181
- #
182
- # module Commenting
183
- # def self.included(base)
184
- # base.list :comments, Ohm::Model::Wrapper.new(:Comment) {
185
- # Object.const_get(:Comment)
186
- # }
187
- # end
188
- # end
189
- #
190
- # # In your classes:
191
- # class Post < Ohm::Model
192
- # include Commenting
193
- # end
194
- #
195
- # class Comment < Ohm::Model
196
- # end
197
- #
198
- # p = Post.create
199
- # p.comments.empty?
200
- # # => true
201
- #
202
- # p.comments.push(Comment.create)
203
- # p.comments.size == 1
204
- # # => true
205
- #
206
- # @param [Symbol, String] name Canonical name of wrapped class.
207
- # @param [#to_proc] block Closure for getting the name of the constant.
208
- def initialize(name, &block)
209
- @name = name
210
- @caller = ::Kernel.caller[2]
211
- @block = block
212
-
213
- class << self
214
- def method_missing(method_id, *args)
215
- ::Kernel.raise(
216
- ::NoMethodError,
217
- "You tried to call %s#%s, but %s is not defined on %s" % [
218
- @name, method_id, @name, @caller
219
- ]
220
- )
221
- end
222
- end
223
- end
224
-
225
- # Used as a convenience for wrapping an existing constant into a
226
- # {Ohm::Model::Wrapper wrapper object}.
227
- #
228
- # This is used extensively within the library for points where a user
229
- # defined class (e.g. _Post_, _User_, _Comment_) is expected.
230
- #
231
- # You can also use this if you need to do uncommon things, such as
232
- # creating your own {Ohm::Model::Set Set}, {Ohm::Model::List List}, etc.
233
- #
234
- # (*NOTE:* Keep in mind that the following code is given only as an
235
- # educational example, and is in no way prescribed as good design.)
236
- #
237
- # class User < Ohm::Model
238
- # end
239
- #
240
- # User.create(:id => "1001")
241
- #
242
- # Ohm.redis.sadd("myset", 1001)
243
- #
244
- # key = Ohm::Key.new("myset", Ohm.redis)
245
- # set = Ohm::Model::Set.new(key, Ohm::Model::Wrapper.wrap(User))
246
- #
247
- # [User[1001]] == set.all.to_a
248
- # # => true
249
- #
250
- # @see http://ohm.keyvalue.org/tutorials/chaining Chaining Ohm Sets
251
- def self.wrap(object)
252
- object.class == self ? object : new(object.inspect) { object }
253
- end
254
-
255
- # Evaluates the passed block in {Ohm::Model::Wrapper#initialize}.
256
- #
257
- # @return [Class] The wrapped class.
258
- def unwrap
259
- @block.call
260
- end
261
-
262
- # Since {Ohm::Model::Wrapper} is a subclass of _BasicObject_ we have
263
- # to manually declare this.
264
- #
265
- # @return [Wrapper]
266
- def class
267
- Wrapper
268
- end
132
+ # Defines most of the methods used by `Set` and `MultiSet`.
133
+ module Collection
134
+ include Enumerable
269
135
 
270
- # @return [String] A string describing this lazy object.
271
- def inspect
272
- "<Wrapper for #{@name} (in #{@caller})>"
273
- end
136
+ # Fetch the data from Redis in one go.
137
+ def to_a
138
+ fetch(ids)
274
139
  end
275
140
 
276
- # Defines the base implementation for all enumerable types in Ohm,
277
- # which includes {Ohm::Model::Set Sets}, {Ohm::Model::List Lists} and
278
- # {Ohm::Model::Index Indices}.
279
- class Collection
280
- include Enumerable
281
-
282
- # An instance of {Ohm::Key}.
283
- attr :key
284
-
285
- # A subclass of {Ohm::Model}.
286
- attr :model
141
+ def each
142
+ to_a.each { |e| yield e }
143
+ end
287
144
 
288
- # @param [Key] key A key which includes a _Redis_ connection.
289
- # @param [Ohm::Model::Wrapper] model A wrapped subclass of {Ohm::Model}.
290
- def initialize(key, model)
291
- @key = key
292
- @model = model.unwrap
293
- end
145
+ def empty?
146
+ size == 0
147
+ end
294
148
 
295
- # Adds an instance of {Ohm::Model} to this collection.
296
- #
297
- # @param [#id] model A model with an ID.
298
- def add(model)
299
- self << model
300
- end
149
+ # Allows you to sort by any field in your model.
150
+ #
151
+ # Example:
152
+ #
153
+ # class User < Ohm::Model
154
+ # attribute :name
155
+ # end
156
+ #
157
+ # User.all.sort_by(:name, order: "ALPHA")
158
+ # User.all.sort_by(:name, order: "ALPHA DESC")
159
+ # User.all.sort_by(:name, order: "ALPHA DESC", limit: [0, 10])
160
+ #
161
+ # Note: This is slower compared to just doing `sort`, specifically
162
+ # because Redis has to read each individual hash in order to sort
163
+ # them.
164
+ #
165
+ def sort_by(att, options = {})
166
+ sort(options.merge(by: namespace["*->%s" % att]))
167
+ end
301
168
 
302
- # Sort this collection using the ID by default, or an attribute defined
303
- # in the elements of this collection.
304
- #
305
- # *NOTE:* It is worth mentioning that if you want to sort by a specific
306
- # attribute instead of an ID, you would probably want to use
307
- # {Ohm::Model::Collection#sort_by sort_by} instead.
308
- #
309
- # @example
310
- # class Post < Ohm::Model
311
- # attribute :title
312
- # end
313
- #
314
- # p1 = Post.create(:title => "Alpha")
315
- # p2 = Post.create(:title => "Beta")
316
- # p3 = Post.create(:title => "Gamma")
317
- #
318
- # [p1, p2, p3] == Post.all.sort.to_a
319
- # # => true
320
- #
321
- # [p3, p2, p1] == Post.all.sort(:order => "DESC").to_a
322
- # # => true
323
- #
324
- # [p1, p2, p3] == Post.all.sort(:by => "Post:*->title",
325
- # :order => "ASC ALPHA").to_a
326
- # # => true
327
- #
328
- # [p3, p2, p1] == Post.all.sort(:by => "Post:*->title",
329
- # :order => "DESC ALPHA").to_a
330
- # # => true
331
- #
332
- # @see file:README.html#sorting Sorting in the README.
333
- # @see http://code.google.com/p/redis/wiki/SortCommand SORT in the
334
- # Redis Command Reference.
335
- def sort(options = {})
336
- return [] unless key.exists
337
-
338
- opts = options.dup
339
- opts[:start] ||= 0
340
- opts[:limit] = [opts[:start], opts[:limit]] if opts[:limit]
341
-
342
- key.sort(opts).map(&model)
169
+ # Allows you to sort your models using their IDs. This is much
170
+ # faster than `sort_by`. If you simply want to get records in
171
+ # ascending or descending order, then this is the best method to
172
+ # do that.
173
+ #
174
+ # Example:
175
+ #
176
+ # class User < Ohm::Model
177
+ # attribute :name
178
+ # end
179
+ #
180
+ # User.create(name: "John")
181
+ # User.create(name: "Jane")
182
+ #
183
+ # User.all.sort.map(&:id) == ["1", "2"]
184
+ # # => true
185
+ #
186
+ # User.all.sort(order: "ASC").map(&:id) == ["1", "2"]
187
+ # # => true
188
+ #
189
+ # User.all.sort(order: "DESC").map(&:id) == ["2", "1"]
190
+ # # => true
191
+ #
192
+ def sort(options = {})
193
+ if options.has_key?(:get)
194
+ options[:get] = namespace["*->%s" % options[:get]]
195
+ return execute { |key| key.sort(options) }
343
196
  end
344
197
 
345
- # Sort the model instances by the given attribute.
346
- #
347
- # @example Sorting elements by name:
348
- #
349
- # User.create :name => "B"
350
- # User.create :name => "A"
351
- #
352
- # user = User.all.sort_by(:name, :order => "ALPHA").first
353
- # user.name == "A"
354
- # # => true
355
- #
356
- # @see file:README.html#sorting Sorting in the README.
357
- def sort_by(att, options = {})
358
- return [] unless key.exists
359
-
360
- opts = options.dup
361
- opts.merge!(:by => model.key["*->#{att}"])
362
-
363
- if opts[:get]
364
- key.sort(opts.merge(:get => model.key["*->#{opts[:get]}"]))
365
- else
366
- sort(opts)
367
- end
368
- end
198
+ fetch(execute { |key| key.sort(options) })
199
+ end
369
200
 
370
- # Delete this collection.
371
- #
372
- # @example
373
- #
374
- # class Post < Ohm::Model
375
- # list :comments, Comment
376
- # end
377
- #
378
- # class Comment < Ohm::Model
379
- # end
380
- #
381
- # post = Post.create
382
- # post.comments << Comment.create
383
- #
384
- # post.comments.size == 1
385
- # # => true
386
- #
387
- # post.comments.clear
388
- # post.comments.size == 0
389
- # # => true
390
- # @see http://code.google.com/p/redis/wiki/DelCommand DEL in the Redis
391
- # Command Reference.
392
- def clear
393
- key.del
394
- end
201
+ # Check if a model is included in this set.
202
+ #
203
+ # Example:
204
+ #
205
+ # u = User.create
206
+ #
207
+ # User.all.include?(u)
208
+ # # => true
209
+ #
210
+ # Note: Ohm simply checks that the model's ID is included in the
211
+ # set. It doesn't do any form of type checking.
212
+ #
213
+ def include?(model)
214
+ exists?(model.id)
215
+ end
395
216
 
396
- # Simultaneously clear and add all models. This wraps all operations
397
- # in a MULTI EXEC block to make the whole operation atomic.
398
- #
399
- # @example
400
- #
401
- # class Post < Ohm::Model
402
- # list :comments, Comment
403
- # end
404
- #
405
- # class Comment < Ohm::Model
406
- # end
407
- #
408
- # post = Post.create
409
- # post.comments << Comment.create(:id => 100)
410
- #
411
- # post.comments.map(&:id) == ["100"]
412
- # # => true
413
- #
414
- # comments = (101..103).to_a.map { |i| Comment.create(:id => i) }
415
- #
416
- # post.comments.replace(comments)
417
- # post.comments.map(&:id) == ["101", "102", "103"]
418
- # # => true
419
- #
420
- # @see http://code.google.com/p/redis/wiki/MultiExecCommand MULTI EXEC
421
- # in the Redis Command Reference.
422
- def replace(models)
423
- model.db.multi do
424
- clear
425
- models.each { |model| add(model) }
426
- end
427
- end
217
+ # Returns the total size of the set using SCARD.
218
+ def size
219
+ execute { |key| key.scard }
220
+ end
428
221
 
429
- # @return [true, false] Whether or not this collection is empty.
430
- def empty?
431
- !key.exists
432
- end
222
+ # Syntactic sugar for `sort_by` or `sort` when you only need the
223
+ # first element.
224
+ #
225
+ # Example:
226
+ #
227
+ # User.all.first ==
228
+ # User.all.sort(limit: [0, 1]).first
229
+ #
230
+ # User.all.first(by: :name, "ALPHA") ==
231
+ # User.all.sort_by(:name, order: "ALPHA", limit: [0, 1]).first
232
+ #
233
+ def first(options = {})
234
+ opts = options.dup
235
+ opts.merge!(limit: [0, 1])
433
236
 
434
- # @return [Array] Array representation of this collection.
435
- def to_a
436
- all
237
+ if opts[:by]
238
+ sort_by(opts.delete(:by), opts).first
239
+ else
240
+ sort(opts).first
437
241
  end
438
242
  end
439
243
 
440
- # Provides a Ruby-esque interface to a _Redis_ *SET*. The *SET* is assumed
441
- # to be composed of ids which maps to {#model}.
442
- class Set < Collection
443
- # An implementation which relies on *SMEMBERS* and yields an instance
444
- # of {#model}.
445
- #
446
- # @example
447
- #
448
- # class Author < Ohm::Model
449
- # set :poems, Poem
450
- # end
451
- #
452
- # class Poem < Ohm::Model
453
- # end
454
- #
455
- # neruda = Author.create
456
- # neruda.poems.add(Poem.create)
457
- #
458
- # neruda.poems.each do |poem|
459
- # # do something with the poem
460
- # end
461
- #
462
- # # if you look at the source, you'll quickly see that this can
463
- # # easily be achieved by doing the following:
464
- #
465
- # neruda.poems.key.smembers.each do |id|
466
- # poem = Poem[id]
467
- # # do something with the poem
468
- # end
469
- #
470
- # @see http://code.google.com/p/redis/wiki/SmembersCommand SMEMBERS
471
- # in Redis Command Reference.
472
- def each(&block)
473
- key.smembers.each { |id| block.call(model.to_proc[id]) }
474
- end
244
+ # Grab all the elements of this set using SMEMBERS.
245
+ def ids
246
+ execute { |key| key.smembers }
247
+ end
475
248
 
476
- # Convenient way to scope access to a predefined set, useful for access
477
- # control.
478
- #
479
- # @example
480
- #
481
- # class User < Ohm::Model
482
- # set :photos, Photo
483
- # end
484
- #
485
- # class Photo < Ohm::Model
486
- # end
487
- #
488
- # @user = User.create
489
- # @user.photos.add(Photo.create(:id => "101"))
490
- # @user.photos.add(Photo.create(:id => "102"))
491
- #
492
- # Photo.create(:id => "500")
493
- #
494
- # @user.photos[101] == Photo[101]
495
- # # => true
496
- #
497
- # @user.photos[500] == nil
498
- # # => true
499
- #
500
- # @param [#to_s] id Any id existing within this set.
501
- # @return [Ohm::Model, nil] The model if it exists.
502
- def [](id)
503
- model[id] if key.sismember(id)
504
- end
249
+ # Retrieve a specific element using an ID from this set.
250
+ #
251
+ # Example:
252
+ #
253
+ # # Let's say we got the ID 1 from a request parameter.
254
+ # id = 1
255
+ #
256
+ # # Retrieve the post if it's included in the user's posts.
257
+ # post = user.posts[id]
258
+ #
259
+ def [](id)
260
+ model[id] if exists?(id)
261
+ end
505
262
 
506
- # Adds a model to this set.
507
- #
508
- # @param [#id] model Typically an instance of an {Ohm::Model} subclass.
509
- #
510
- # @see http://code.google.com/p/redis/wiki/SaddCommand SADD in Redis
511
- # Command Reference.
512
- def <<(model)
513
- key.sadd(model.id)
514
- end
515
- alias add <<
516
-
517
- # Thin Ruby interface wrapper for *SCARD*.
518
- #
519
- # @return [Fixnum] The total number of members for this set.
520
- # @see http://code.google.com/p/redis/wiki/ScardCommand SCARD in Redis
521
- # Command Reference.
522
- def size
523
- key.scard
524
- end
263
+ private
264
+ def exists?(id)
265
+ execute { |key| key.sismember(id) }
266
+ end
525
267
 
526
- # Thin Ruby interface wrapper for *SREM*.
527
- #
528
- # @param [#id] member a member of this set.
529
- # @see http://code.google.com/p/redis/wiki/SremCommand SREM in Redis
530
- # Command Reference.
531
- def delete(member)
532
- key.srem(member.id)
268
+ def fetch(ids)
269
+ arr = model.db.pipelined do
270
+ ids.each { |id| namespace[id].hgetall }
533
271
  end
534
272
 
535
- # Array representation of this set.
536
- #
537
- # @example
538
- #
539
- # class Author < Ohm::Model
540
- # set :posts, Post
541
- # end
542
- #
543
- # class Post < Ohm::Model
544
- # end
545
- #
546
- # author = Author.create
547
- # author.posts.add(Author.create(:id => "101"))
548
- # author.posts.add(Author.create(:id => "102"))
549
- #
550
- # author.posts.all.is_a?(Array)
551
- # # => true
552
- #
553
- # author.posts.all.include?(Author[101])
554
- # # => true
555
- #
556
- # author.posts.all.include?(Author[102])
557
- # # => true
558
- #
559
- # @return [Array<Ohm::Model>] All members of this set.
560
- def all
561
- key.smembers.map(&model)
562
- end
273
+ return [] if arr.nil?
563
274
 
564
- # Allows you to find members of this set which fits the given criteria.
565
- #
566
- # @example
567
- #
568
- # class Post < Ohm::Model
569
- # attribute :title
570
- # attribute :tags
571
- #
572
- # index :title
573
- # index :tag
574
- #
575
- # def tag
576
- # tags.split(/\s+/)
577
- # end
578
- # end
579
- #
580
- # post = Post.create(:title => "Ohm", :tags => "ruby ohm redis")
581
- # Post.all.is_a?(Ohm::Model::Set)
582
- # # => true
583
- #
584
- # Post.all.find(:tag => "ruby").include?(post)
585
- # # => true
586
- #
587
- # # Post.find is actually just a wrapper around Post.all.find
588
- # Post.find(:tag => "ohm", :title => "Ohm").include?(post)
589
- # # => true
590
- #
591
- # Post.find(:tag => ["ruby", "python"]).empty?
592
- # # => true
593
- #
594
- # # Alternatively, you may choose to chain them later on.
595
- # ruby = Post.find(:tag => "ruby")
596
- # ruby.find(:title => "Ohm").include?(post)
597
- # # => true
598
- #
599
- # @param [Hash] options A hash of key value pairs.
600
- # @return [Ohm::Model::Set] A set satisfying the filter passed.
601
- def find(options)
602
- source = keys(options)
603
- target = source.inject(key.volatile) { |chain, other| chain + other }
604
- apply(:sinterstore, key, source, target)
275
+ arr.map.with_index do |atts, idx|
276
+ model.new(atts.update(id: ids[idx]))
605
277
  end
278
+ end
279
+ end
606
280
 
607
- # Similar to find except that it negates the criteria.
608
- #
609
- # @example
610
- # class Post < Ohm::Model
611
- # attribute :title
612
- # end
613
- #
614
- # ohm = Post.create(:title => "Ohm")
615
- # ruby = Post.create(:title => "Ruby")
616
- #
617
- # Post.except(:title => "Ohm").include?(ruby)
618
- # # => true
619
- #
620
- # Post.except(:title => "Ohm").size == 1
621
- # # => true
622
- #
623
- # @param [Hash] options A hash of key value pairs.
624
- # @return [Ohm::Model::Set] A set satisfying the filter passed.
625
- def except(options)
626
- source = keys(options)
627
- target = source.inject(key.volatile) { |chain, other| chain - other }
628
- apply(:sdiffstore, key, source, target)
629
- end
281
+ class Set < Struct.new(:key, :namespace, :model)
282
+ include Collection
630
283
 
631
- # Returns by default the lowest id value for this set. You may also
632
- # pass in options similar to {#sort}.
633
- #
634
- # @example
635
- #
636
- # class Post < Ohm::Model
637
- # attribute :title
638
- # end
639
- #
640
- # p1 = Post.create(:id => "101", :title => "Alpha")
641
- # p2 = Post.create(:id => "100", :title => "Beta")
642
- # p3 = Post.create(:id => "99", :title => "Gamma")
643
- #
644
- # Post.all.is_a?(Ohm::Model::Set)
645
- # # => true
646
- #
647
- # p3 == Post.all.first
648
- # # => true
649
- #
650
- # p1 == Post.all.first(:order => "DESC")
651
- # # => true
652
- #
653
- # p1 == Post.all.first(:by => :title, :order => "ASC ALPHA")
654
- # # => true
655
- #
656
- # # just ALPHA also means ASC ALPHA, for brevity.
657
- # p1 == Post.all.first(:by => :title, :order => "ALPHA")
658
- # # => true
659
- #
660
- # p3 == Post.all.first(:by => :title, :order => "DESC ALPHA")
661
- # # => true
662
- #
663
- # @param [Hash] options Sort options hash.
664
- # @return [Ohm::Model, nil] an {Ohm::Model} instance or nil if this
665
- # set is empty.
666
- #
667
- # @see file:README.html#sorting Sorting in the README.
668
- def first(options = {})
669
- opts = options.dup
670
- opts.merge!(:limit => 1)
671
-
672
- if opts[:by]
673
- sort_by(opts.delete(:by), opts).first
674
- else
675
- sort(opts).first
676
- end
677
- end
284
+ # Add a model directly to the set.
285
+ #
286
+ # Example:
287
+ #
288
+ # user = User.create
289
+ # post = Post.create
290
+ #
291
+ # user.posts.add(post)
292
+ #
293
+ def add(model)
294
+ key.sadd(model.id)
295
+ end
678
296
 
679
- # Ruby-like interface wrapper around *SISMEMBER*.
680
- #
681
- # @param [#id] model Typically an {Ohm::Model} instance.
682
- #
683
- # @return [true, false] Whether or not the {Ohm::Model model} instance
684
- # is a member of this set.
685
- #
686
- # @see http://code.google.com/p/redis/wiki/SismemberCommand SISMEMBER
687
- # in Redis Command Reference.
688
- def include?(model)
689
- key.sismember(model.id)
690
- end
297
+ # Chain new fiters on an existing set.
298
+ #
299
+ # Example:
300
+ #
301
+ # set = User.find(name: "John")
302
+ # set.find(age: 30)
303
+ #
304
+ def find(dict)
305
+ keys = model.filters(dict)
306
+ keys.push(key)
691
307
 
692
- def inspect
693
- "#<Set (#{model}): #{key.smembers.inspect}>"
694
- end
308
+ MultiSet.new(keys, namespace, model)
309
+ end
695
310
 
696
- protected
697
- # @private
698
- def apply(operation, key, source, target)
699
- target.send(operation, key, *source)
700
- Set.new(target, Wrapper.wrap(model))
701
- end
311
+ # Replace all the existing elements of a set with a different
312
+ # collection of models. This happens atomically in a MULTI-EXEC
313
+ # block.
314
+ #
315
+ # Example:
316
+ #
317
+ # user = User.create
318
+ # p1 = Post.create
319
+ # user.posts.add(p1)
320
+ #
321
+ # p2, p3 = Post.create, Post.create
322
+ # user.posts.replace([p2, p3])
323
+ #
324
+ # user.posts.include?(p1)
325
+ # # => false
326
+ #
327
+ def replace(models)
328
+ ids = models.map { |model| model.id }
702
329
 
703
- # @private
704
- #
705
- # Transform a hash of attribute/values into an array of keys.
706
- def keys(hash)
707
- [].tap do |keys|
708
- hash.each do |key, values|
709
- values = [values] unless values.kind_of?(Array)
710
- values.each do |v|
711
- keys << model.index_key_for(key, v)
712
- end
713
- end
714
- end
330
+ key.redis.multi do
331
+ key.del
332
+ ids.each { |id| key.sadd(id) }
715
333
  end
716
334
  end
717
335
 
718
- class Index < Set
719
- # This method is here primarily as an optimization. Let's say you have
720
- # the following model:
721
- #
722
- # class Post < Ohm::Model
723
- # attribute :title
724
- # index :title
725
- # end
726
- #
727
- # ruby = Post.create(:title => "ruby")
728
- # redis = Post.create(:title => "redis")
729
- #
730
- # Post.key[:all].smembers == [ruby.id, redis.id]
731
- # # => true
732
- #
733
- # Post.index_key_for(:title, "ruby").smembers == [ruby.id]
734
- # # => true
735
- #
736
- # Post.index_key_for(:title, "redis").smembers == [redis.id]
737
- # # => true
738
- #
739
- # If we want to search for example all `Posts` entitled "ruby" or
740
- # "redis", then it doesn't make sense to do an INTERSECTION with
741
- # `Post.key[:all]` since it would be redundant.
742
- #
743
- # The implementation of {Ohm::Model::Index#find} avoids this redundancy.
744
- #
745
- # @see Ohm::Model::Set#find find in Ohm::Model::Set.
746
- # @see Ohm::Model.find find in Ohm::Model.
747
- def find(options)
748
- keys = keys(options)
749
- return super(options) if keys.size > 1
750
-
751
- Set.new(keys.first, Wrapper.wrap(model))
752
- end
336
+ private
337
+ def execute
338
+ yield key
753
339
  end
340
+ end
754
341
 
755
- # Provides a Ruby-esque interface to a _Redis_ *LIST*. The *LIST* is
756
- # assumed to be composed of ids which maps to {#model}.
757
- class List < Collection
758
- # An implementation which relies on *LRANGE* and yields an instance
759
- # of {#model}.
760
- #
761
- # @example
762
- #
763
- # class Post < Ohm::Model
764
- # list :comments, Comment
765
- # end
766
- #
767
- # class Comment < Ohm::Model
768
- # end
769
- #
770
- # post = Post.create
771
- # post.comments.add(Comment.create)
772
- # post.comments.add(Comment.create)
773
- #
774
- # post.comments.each do |comment|
775
- # # do something with the comment
776
- # end
777
- #
778
- # # reading the source reveals that this is achieved by doing:
779
- # post.comments.key.lrange(0, -1).each do |id|
780
- # comment = Comment[id]
781
- # # do something with the comment
782
- # end
783
- #
784
- # @see http://code.google.com/p/redis/wiki/LrangeCommand LRANGE
785
- # in Redis Command Reference.
786
- def each(&block)
787
- key.lrange(0, -1).each { |id| block.call(model.to_proc[id]) }
788
- end
789
-
790
- # Thin wrapper around *RPUSH*.
791
- #
792
- # @example
793
- #
794
- # class Post < Ohm::Model
795
- # list :comments, Comment
796
- # end
797
- #
798
- # class Comment < Ohm::Model
799
- # end
800
- #
801
- # p = Post.create
802
- # p.comments << Comment.create
803
- #
804
- # @param [#id] model Typically an {Ohm::Model} instance.
805
- #
806
- # @see http://code.google.com/p/redis/wiki/RpushCommand RPUSH
807
- # in Redis Command Reference.
808
- def <<(model)
809
- key.rpush(model.id)
810
- end
811
- alias push <<
812
-
813
- # Returns the element at index, or returns a subarray starting at
814
- # `start` and continuing for `length` elements, or returns a subarray
815
- # specified by `range`. Negative indices count backward from the end
816
- # of the array (-1 is the last element). Returns nil if the index
817
- # (or starting index) are out of range.
818
- #
819
- # @example
820
- # class Post < Ohm::Model
821
- # list :comments, Comment
822
- # end
823
- #
824
- # class Comment < Ohm::Model
825
- # end
826
- #
827
- # post = Post.create
828
- #
829
- # 10.times { post.comments << Comment.create }
830
- #
831
- # post.comments[0] == Comment[1]
832
- # # => true
833
- #
834
- # post.comments[0, 4] == (1..5).map { |i| Comment[i] }
835
- # # => true
836
- #
837
- # post.comments[0, 4] == post.comments[0..4]
838
- # # => true
839
- #
840
- # post.comments.all == post.comments[0, -1]
841
- # # => true
842
- #
843
- # @see http://code.google.com/p/redis/wiki/LrangeCommand LRANGE
844
- # in Redis Command Reference.
845
- def [](index, limit = nil)
846
- case [index, limit]
847
- when Pattern[Fixnum, Fixnum] then
848
- key.lrange(index, limit).collect { |id| model.to_proc[id] }
849
- when Pattern[Range, nil] then
850
- key.lrange(index.first, index.last).collect { |id| model.to_proc[id] }
851
- when Pattern[Fixnum, nil] then
852
- model[key.lindex(index)]
853
- end
854
- end
855
-
856
- # Convience method for doing list[0], similar to Ruby's Array#first
857
- # method.
858
- #
859
- # @return [Ohm::Model, nil] An {Ohm::Model} instance or nil if the list
860
- # is empty.
861
- def first
862
- self[0]
863
- end
342
+ # Anytime you filter a set with more than one requirement, you
343
+ # internally use a `MultiSet`. `MutiSet` is a bit slower than just
344
+ # a `Set` because it has to `SINTERSTORE` all the keys prior to
345
+ # retrieving the members, size, etc.
346
+ #
347
+ # Example:
348
+ #
349
+ # User.all.kind_of?(Ohm::Set)
350
+ # # => true
351
+ #
352
+ # User.find(name: "John").kind_of?(Ohm::Set)
353
+ # # => true
354
+ #
355
+ # User.find(name: "John", age: 30).kind_of?(Ohm::MultiSet)
356
+ # # => true
357
+ #
358
+ class MultiSet < Struct.new(:keys, :namespace, :model)
359
+ include Collection
864
360
 
865
- # Returns the model at the tail of this list, while simultaneously
866
- # removing it from the list.
867
- #
868
- # @return [Ohm::Model, nil] an {Ohm::Model} instance or nil if the list
869
- # is empty.
870
- #
871
- # @see http://code.google.com/p/redis/wiki/LpopCommand RPOP
872
- # in Redis Command Reference.
873
- def pop
874
- model[key.rpop]
875
- end
361
+ # Chain new fiters on an existing set.
362
+ #
363
+ # Example:
364
+ #
365
+ # set = User.find(name: "John", age: 30)
366
+ # set.find(status: 'pending')
367
+ #
368
+ def find(dict)
369
+ keys = model.filters(dict)
370
+ keys.push(*self.keys)
876
371
 
877
- # Returns the model at the head of this list, while simultaneously
878
- # removing it from the list.
879
- #
880
- # @return [Ohm::Model, nil] An {Ohm::Model} instance or nil if the list
881
- # is empty.
882
- #
883
- # @see http://code.google.com/p/redis/wiki/LpopCommand LPOP
884
- # in Redis Command Reference.
885
- def shift
886
- model[key.lpop]
887
- end
372
+ MultiSet.new(keys, namespace, model)
373
+ end
888
374
 
889
- # Prepends an {Ohm::Model} instance at the beginning of this list.
890
- #
891
- # @param [#id] model Typically an {Ohm::Model} instance.
892
- #
893
- # @see http://code.google.com/p/redis/wiki/RpushCommand LPUSH
894
- # in Redis Command Reference.
895
- def unshift(model)
896
- key.lpush(model.id)
897
- end
375
+ private
376
+ def execute
377
+ key = namespace[:temp][SecureRandom.uuid]
378
+ key.sinterstore(*keys)
898
379
 
899
- # Returns an array representation of this list, with elements of the
900
- # array being an instance of {#model}.
901
- #
902
- # @return [Array<Ohm::Model>] Instances of {Ohm::Model}.
903
- def all
904
- key.lrange(0, -1).map(&model)
380
+ begin
381
+ yield key
382
+ ensure
383
+ key.del
905
384
  end
385
+ end
386
+ end
906
387
 
907
- # Thin Ruby interface wrapper for *LLEN*.
908
- #
909
- # @return [Fixnum] The total number of elements for this list.
910
- #
911
- # @see http://code.google.com/p/redis/wiki/LlenCommand LLEN in Redis
912
- # Command Reference.
913
- def size
914
- key.llen
915
- end
388
+ # The base class for all your models. In order to better understand
389
+ # it, here is a semi-realtime explanation of the details involved
390
+ # when creating a User instance.
391
+ #
392
+ # Example:
393
+ #
394
+ # class User < Ohm::Model
395
+ # attribute :name
396
+ # index :name
397
+ #
398
+ # attribute :email
399
+ # unique :email
400
+ #
401
+ # counter :points
402
+ #
403
+ # set :posts, :Post
404
+ # end
405
+ #
406
+ # u = User.create(name: "John", email: "foo@bar.com")
407
+ # u.incr :points
408
+ # u.posts.add(Post.create)
409
+ #
410
+ # When you execute `User.create(...)`, you run the following Redis
411
+ # commands:
412
+ #
413
+ # # Generate an ID
414
+ # INCR User:id
415
+ #
416
+ # # Add the newly generated ID, (let's assume the ID is 1).
417
+ # SADD User:all 1
418
+ #
419
+ # # Store the unique index
420
+ # HSET User:uniques:email foo@bar.com 1
421
+ #
422
+ # # Store the name index
423
+ # SADD User:indices:name:John 1
424
+ #
425
+ # # Store the HASH
426
+ # HMSET User:1 name John email foo@bar.com
427
+ #
428
+ # Next we increment points:
429
+ #
430
+ # HINCR User:1:counters points 1
431
+ #
432
+ # And then we add a Post to the `posts` set.
433
+ # (For brevity, let's assume the Post created has an ID of 1).
434
+ #
435
+ # SADD User:1:posts 1
436
+ #
437
+ class Model
438
+ include Scrivener::Validations
916
439
 
917
- # Ruby-like interface wrapper around *LRANGE*.
918
- #
919
- # @param [#id] model Typically an {Ohm::Model} instance.
920
- #
921
- # @return [true, false] Whether or not the {Ohm::Model} instance is
922
- # an element of this list.
923
- #
924
- # @see http://code.google.com/p/redis/wiki/LrangeCommand LRANGE
925
- # in Redis Command Reference.
926
- def include?(model)
927
- key.lrange(0, -1).include?(model.id)
928
- end
440
+ def self.conn
441
+ @conn ||= Connection.new(name, Ohm.conn.options)
442
+ end
929
443
 
930
- def inspect
931
- "#<List (#{model}): #{key.lrange(0, -1).inspect}>"
932
- end
444
+ def self.connect(options)
445
+ @key = nil
446
+ @lua = nil
447
+ conn.start(options)
933
448
  end
934
449
 
935
- # All validations that need access to the _Redis_ database go here.
936
- # As of this writing, {Ohm::Model::Validations#assert_unique} is the only
937
- # assertion contained within this module.
938
- module Validations
939
- include Ohm::Validations
940
-
941
- # Validates that the attribute or array of attributes are unique. For
942
- # this, an index of the same kind must exist.
943
- #
944
- # @overload assert_unique :name
945
- # Validates that the name attribute is unique.
946
- # @overload assert_unique [:street, :city]
947
- # Validates that the :street and :city pair is unique.
948
- def assert_unique(atts, error = [atts, :not_unique])
949
- indices = Array(atts).map { |att| index_key_for(att, send(att)) }
950
- result = db.sinter(*indices)
951
-
952
- assert result.empty? || !new? && result.include?(id.to_s), error
953
- end
450
+ def self.db
451
+ conn.redis
954
452
  end
955
453
 
956
- include Validations
454
+ def self.lua
455
+ @lua ||= Lua.new(File.join(Dir.pwd, "lua"), db)
456
+ end
957
457
 
958
- # Raised when you try and get the *id* of an {Ohm::Model} without an id.
458
+ # The namespace for all the keys generated using this model.
959
459
  #
960
- # class Post < Ohm::Model
961
- # list :comments, Comment
962
- # end
963
- #
964
- # class Comment < Ohm::Model
965
- # end
460
+ # Example:
966
461
  #
967
- # ex = nil
968
- # begin
969
- # Post.new.id
970
- # rescue Exception => e
971
- # ex = e
972
- # end
462
+ # class User < Ohm::Model
973
463
  #
974
- # ex.kind_of?(Ohm::Model::MissingID)
464
+ # User.key == "User"
465
+ # User.key.kind_of?(String)
975
466
  # # => true
976
467
  #
977
- # This is also one of the most common errors you'll be faced with when
978
- # you're new to {Ohm} coming from an ActiveRecord background, where you
979
- # are used to just assigning associations even before the base model is
980
- # persisted.
981
- #
982
- # # following from the example above:
983
- # post = Post.new
984
- #
985
- # ex = nil
986
- # begin
987
- # post.comments << Comment.new
988
- # rescue Exception => e
989
- # ex = e
990
- # end
991
- #
992
- # ex.kind_of?(Ohm::Model::MissingID)
468
+ # User.key.kind_of?(Nest)
993
469
  # # => true
994
470
  #
995
- # # Correct way:
996
- # post = Post.new
471
+ # To find out more about Nest, see:
472
+ # http://github.com/soveran/nest
997
473
  #
998
- # if post.save
999
- # post.comments << Comment.create
1000
- # end
1001
- class MissingID < Error
1002
- def message
1003
- "You tried to perform an operation that needs the model ID, " +
1004
- "but it's not present."
1005
- end
474
+ def self.key
475
+ @key ||= Nest.new(self.name, db)
1006
476
  end
1007
477
 
1008
- # Raised when you try and do an {Ohm::Model::Set#find} operation and use
1009
- # a key which you did not define as an index.
478
+ # Retrieve a record by ID.
1010
479
  #
1011
- # class Post < Ohm::Model
1012
- # attribute :title
1013
- # end
480
+ # Example:
1014
481
  #
1015
- # post = Post.create(:title => "Ohm")
482
+ # u = User.create
483
+ # u == User[u.id]
484
+ # # => true
1016
485
  #
1017
- # ex = nil
1018
- # begin
1019
- # Post.find(:title => "Ohm")
1020
- # rescue Exception => e
1021
- # ex = e
1022
- # end
486
+ def self.[](id)
487
+ new(id: id).load! if id && exists?(id)
488
+ end
489
+
490
+ # Retrieve a set of models given an array of IDs.
1023
491
  #
1024
- # ex.kind_of?(Ohm::Model::IndexNotFound)
1025
- # # => true
492
+ # Example:
1026
493
  #
1027
- # To correct this problem, simply define a _:title_ *index* in your class.
494
+ # ids = [1, 2, 3]
495
+ # ids.map(&User)
1028
496
  #
1029
- # class Post < Ohm::Model
1030
- # attribute :title
1031
- # index :title
1032
- # end
1033
- class IndexNotFound < Error
1034
- def initialize(att)
1035
- @att = att
1036
- end
1037
-
1038
- def message
1039
- "Index #{@att.inspect} not found."
1040
- end
497
+ # Note: The use of this should be a last resort for your actual
498
+ # application runtime, or for simply debugging in your console. If
499
+ # you care about performance, you should pipeline your reads. For
500
+ # more information checkout the implementation of Ohm::Set#fetch.
501
+ #
502
+ def self.to_proc
503
+ lambda { |id| self[id] }
1041
504
  end
1042
505
 
1043
- @@attributes = Hash.new { |hash, key| hash[key] = [] }
1044
- @@collections = Hash.new { |hash, key| hash[key] = [] }
1045
- @@counters = Hash.new { |hash, key| hash[key] = [] }
1046
- @@indices = Hash.new { |hash, key| hash[key] = [] }
1047
-
1048
- def id
1049
- @id or raise MissingID
506
+ # Check if the ID exists within <Model>:all.
507
+ def self.exists?(id)
508
+ key[:all].sismember(id)
1050
509
  end
1051
510
 
1052
- # Defines a string attribute for the model. This attribute will be
1053
- # persisted by _Redis_ as a string. Any value stored here will be
1054
- # retrieved in its string representation.
511
+ # Find values in `unique` indices.
1055
512
  #
1056
- # If you're looking to have typecasting built in, you may want to look at
1057
- # Ohm::Typecast in Ohm::Contrib.
513
+ # Example:
1058
514
  #
1059
- # @param name [Symbol] Name of the attribute.
1060
- # @see http://cyx.github.com/ohm-contrib/doc/Ohm/Typecast.html
1061
- def self.attribute(name)
1062
- define_method(name) do
1063
- read_local(name)
1064
- end
1065
-
1066
- define_method(:"#{name}=") do |value|
1067
- write_local(name, value)
1068
- end
1069
-
1070
- attributes << name unless attributes.include?(name)
1071
- end
1072
-
1073
- # Defines a counter attribute for the model. This attribute can't be
1074
- # assigned, only incremented or decremented. It will be zero by default.
515
+ # class User < Ohm::Model
516
+ # unique :email
517
+ # end
1075
518
  #
1076
- # @param [Symbol] name Name of the counter.
1077
- def self.counter(name)
1078
- define_method(name) do
1079
- read_local(name).to_i
1080
- end
1081
-
1082
- counters << name unless counters.include?(name)
519
+ # u = User.create(email: "foo@bar.com")
520
+ # u == User.with(:email, "foo@bar.com")
521
+ # # => true
522
+ #
523
+ def self.with(att, val)
524
+ id = key[:uniques][att].hget(val)
525
+ id && self[id]
1083
526
  end
1084
527
 
1085
- # Defines a list attribute for the model. It can be accessed only after
1086
- # the model instance is created, or if you assign an :id during object
1087
- # construction.
528
+ # Find values in indexed fields.
1088
529
  #
1089
- # @example
530
+ # Example:
1090
531
  #
1091
- # class Post < Ohm::Model
1092
- # list :comments, Comment
1093
- # end
532
+ # class User < Ohm::Model
533
+ # attribute :email
1094
534
  #
1095
- # class Comment < Ohm::Model
535
+ # attribute :name
536
+ # index :name
537
+ #
538
+ # attribute :status
539
+ # index :status
540
+ #
541
+ # index :provider
542
+ # index :tag
543
+ #
544
+ # def provider
545
+ # email[/@(.*?).com/, 1]
546
+ # end
547
+ #
548
+ # def tag
549
+ # ["ruby", "python"]
550
+ # end
1096
551
  # end
1097
552
  #
1098
- # # WRONG!!!
1099
- # post = Post.new
1100
- # post.comments << Comment.create
553
+ # u = User.create(name: "John", status: "pending", email: "foo@me.com")
554
+ # User.find(provider: "me", name: "John", status: "pending").include?(u)
555
+ # # => true
1101
556
  #
1102
- # # Right :-)
1103
- # post = Post.create
1104
- # post.comments << Comment.create
557
+ # User.find(tag: "ruby").include?(u)
558
+ # # => true
1105
559
  #
1106
- # # Alternative way if you want to have custom ids.
1107
- # post = Post.new(:id => "my-id")
1108
- # post.comments << Comment.create
1109
- # post.create
560
+ # User.find(tag: "python").include?(u)
561
+ # # => true
1110
562
  #
1111
- # @param [Symbol] name Name of the list.
1112
- def self.list(name, model)
1113
- define_memoized_method(name) { List.new(key[name], Wrapper.wrap(model)) }
1114
- collections << name unless collections.include?(name)
563
+ # User.find(tag: ["ruby", "python"]).include?(u)
564
+ # # => true
565
+ #
566
+ def self.find(dict)
567
+ keys = filters(dict)
568
+
569
+ if keys.size == 1
570
+ Ohm::Set.new(keys.first, key, self)
571
+ else
572
+ Ohm::MultiSet.new(keys, key, self)
573
+ end
1115
574
  end
1116
575
 
1117
- # Defines a set attribute for the model. It can be accessed only after
1118
- # the model instance is created. Sets are recommended when insertion and
1119
- # retreival order is irrelevant, and operations like union, join, and
1120
- # membership checks are important.
1121
- #
1122
- # @param [Symbol] name Name of the set.
1123
- def self.set(name, model)
1124
- define_memoized_method(name) { Set.new(key[name], Wrapper.wrap(model)) }
1125
- collections << name unless collections.include?(name)
576
+ # Index any method on your model. Once you index a method, you can
577
+ # use it in `find` statements.
578
+ def self.index(attribute)
579
+ indices << attribute unless indices.include?(attribute)
1126
580
  end
1127
581
 
1128
- # Creates an index (a set) that will be used for finding instances.
582
+ # Create a unique index for any method on your model. Once you add
583
+ # a unique index, you can use it in `with` statements.
584
+ #
585
+ # Note: if there is a conflict while saving, an
586
+ # `Ohm::UniqueIndexViolation` violation is raised.
1129
587
  #
1130
- # If you want to find a model instance by some attribute value, then an
1131
- # index for that attribute must exist.
588
+ def self.unique(attribute)
589
+ uniques << attribute unless uniques.include?(attribute)
590
+ end
591
+
592
+ # Declare an Ohm::Set with the given name.
1132
593
  #
1133
- # @example
594
+ # Example:
1134
595
  #
1135
596
  # class User < Ohm::Model
1136
- # attribute :email
1137
- # index :email
597
+ # set :posts, :Post
1138
598
  # end
1139
599
  #
1140
- # # Now this is possible:
1141
- # User.find :email => "ohm@example.com"
600
+ # u = User.create
601
+ # u.posts.empty?
602
+ # # => true
603
+ #
604
+ # Note: You can't use the set until you save the model. If you try
605
+ # to do it, you'll receive an Ohm::MissingID error.
1142
606
  #
1143
- # @param [Symbol] name Name of the attribute to be indexed.
1144
- def self.index(att)
1145
- indices << att unless indices.include?(att)
607
+ def self.set(name, model)
608
+ collections << name unless collections.include?(name)
609
+
610
+ define_method name do
611
+ model = Utils.const(self.class, model)
612
+
613
+ Ohm::Set.new(key[name], model.key, model)
614
+ end
1146
615
  end
1147
616
 
1148
- # Define a reference to another object.
617
+ # A macro for defining a method which basically does a find.
1149
618
  #
1150
- # @example
619
+ # Example:
620
+ # class Post < Ohm::Model
621
+ # reference :user, :User
622
+ # end
1151
623
  #
1152
- # class Comment < Ohm::Model
1153
- # attribute :content
1154
- # reference :post, Post
624
+ # class User < Ohm::Model
625
+ # collection :posts, :Post
1155
626
  # end
1156
627
  #
1157
- # @post = Post.create :content => "Interesting stuff"
628
+ # # is the same as
1158
629
  #
1159
- # @comment = Comment.create(:content => "Indeed!", :post => @post)
630
+ # class User < Ohm::Model
631
+ # def posts
632
+ # Post.find(user_id: self.id)
633
+ # end
634
+ # end
1160
635
  #
1161
- # @comment.post.content
1162
- # # => "Interesting stuff"
636
+ def self.collection(name, model, reference = to_reference)
637
+ define_method name do
638
+ model = Utils.const(self.class, model)
639
+ model.find(:"#{reference}_id" => id)
640
+ end
641
+ end
642
+
643
+ # A macro for defining an attribute, an index, and an accessor
644
+ # for a given model.
1163
645
  #
1164
- # @comment.post = Post.create(:content => "Wonderful stuff")
646
+ # Example:
1165
647
  #
1166
- # @comment.post.content
1167
- # # => "Wonderful stuff"
648
+ # class Post < Ohm::Model
649
+ # reference :user, :User
650
+ # end
1168
651
  #
1169
- # @comment.post.update(:content => "Magnific stuff")
652
+ # # It's the same as:
1170
653
  #
1171
- # @comment.post.content
1172
- # # => "Magnific stuff"
654
+ # class Post < Ohm::Model
655
+ # attribute :user_id
656
+ # index :user_id
1173
657
  #
1174
- # @comment.post = nil
658
+ # def user
659
+ # @_memo[:user] ||= User[user_id]
660
+ # end
1175
661
  #
1176
- # @comment.post
1177
- # # => nil
662
+ # def user=(user)
663
+ # self.user_id = user.id
664
+ # @_memo[:user] = user
665
+ # end
666
+ #
667
+ # def user_id=(user_id)
668
+ # @_memo.delete(:user_id)
669
+ # self.user_id = user_id
670
+ # end
671
+ # end
1178
672
  #
1179
- # @see file:README.html#references References Explained.
1180
- # @see Ohm::Model.collection
1181
673
  def self.reference(name, model)
1182
- model = Wrapper.wrap(model)
1183
-
1184
674
  reader = :"#{name}_id"
1185
675
  writer = :"#{name}_id="
1186
676
 
1187
- attributes << reader unless attributes.include?(reader)
1188
-
1189
677
  index reader
1190
678
 
1191
- define_memoized_method(name) do
1192
- model.unwrap[send(reader)]
679
+ define_method(reader) do
680
+ @attributes[reader]
1193
681
  end
1194
682
 
1195
- define_method(:"#{name}=") do |value|
683
+ define_method(writer) do |value|
1196
684
  @_memo.delete(name)
1197
- send(writer, value ? value.id : nil)
685
+ @attributes[reader] = value
1198
686
  end
1199
687
 
1200
- define_method(reader) do
1201
- read_local(reader)
688
+ define_method(:"#{name}=") do |value|
689
+ @_memo.delete(name)
690
+ send(writer, value ? value.id : nil)
1202
691
  end
1203
692
 
1204
- define_method(writer) do |value|
1205
- @_memo.delete(name)
1206
- write_local(reader, value)
693
+ define_method(name) do
694
+ @_memo[name] ||= begin
695
+ model = Utils.const(self.class, model)
696
+ model[send(reader)]
697
+ end
1207
698
  end
1208
699
  end
1209
700
 
1210
- # Define a collection of objects which have a
1211
- # {Ohm::Model.reference reference} to this model.
701
+ # The bread and butter macro of all models. Basically declares
702
+ # persisted attributes. All attributes are stored on the Redis
703
+ # hash.
1212
704
  #
1213
- # class Comment < Ohm::Model
1214
- # attribute :content
1215
- # reference :post, Post
705
+ # Example:
706
+ # class User < Ohm::Model
707
+ # attribute :name
1216
708
  # end
1217
709
  #
1218
- # class Post < Ohm::Model
1219
- # attribute :content
1220
- # collection :comments, Comment
1221
- # reference :author, Person
1222
- # end
710
+ # # It's the same as:
1223
711
  #
1224
- # class Person < Ohm::Model
1225
- # attribute :name
712
+ # class User < Ohm::Model
713
+ # def name
714
+ # @attributes[:name]
715
+ # end
1226
716
  #
1227
- # # When the name of the reference cannot be inferred,
1228
- # # you need to specify it in the third param.
1229
- # collection :posts, Post, :author
717
+ # def name=(name)
718
+ # @attributes[:name] = name
719
+ # end
1230
720
  # end
1231
721
  #
1232
- # @person = Person.create :name => "Albert"
1233
- # @post = Post.create :content => "Interesting stuff",
1234
- # :author => @person
1235
- # @comment = Comment.create :content => "Indeed!", :post => @post
722
+ def self.attribute(name, cast = nil)
723
+ if cast
724
+ define_method(name) do
725
+ cast[@attributes[name]]
726
+ end
727
+ else
728
+ define_method(name) do
729
+ @attributes[name]
730
+ end
731
+ end
732
+
733
+ define_method(:"#{name}=") do |value|
734
+ @attributes[name] = value
735
+ end
736
+ end
737
+
738
+ # Declare a counter. All the counters are internally stored in
739
+ # a different Redis hash, independent from the one that stores
740
+ # the model attributes. Counters are updated with the `incr` and
741
+ # `decr` methods, which interact directly with Redis. Their value
742
+ # can't be assigned as with regular attributes.
1236
743
  #
1237
- # @post.comments.first.content
1238
- # # => "Indeed!"
744
+ # Example:
1239
745
  #
1240
- # @post.author.name
1241
- # # => "Albert"
746
+ # class User < Ohm::Model
747
+ # counter :points
748
+ # end
1242
749
  #
1243
- # *Important*: Please note that even though a collection is a
1244
- # {Ohm::Model::Set set}, you should not add or remove objects from this
1245
- # collection directly.
750
+ # u = User.create
751
+ # u.incr :points
1246
752
  #
1247
- # @see Ohm::Model.reference
1248
- # @param name [Symbol] Name of the collection.
1249
- # @param model [Constant] Model where the reference is defined.
1250
- # @param reference [Symbol] Reference as defined in the associated
1251
- # model.
753
+ # Ohm.redis.hget "User:1:counters", "points"
754
+ # # => 1
1252
755
  #
1253
- # @see file:README.html#collections Collections Explained.
1254
- def self.collection(name, model, reference = to_reference)
1255
- model = Wrapper.wrap(model)
1256
- define_method(name) {
1257
- model.unwrap.find(:"#{reference}_id" => send(:id))
1258
- }
1259
- end
1260
-
1261
- # Used by {Ohm::Model.collection} to infer the reference.
756
+ # Note: You can't use counters until you save the model. If you
757
+ # try to do it, you'll receive an Ohm::MissingID error.
1262
758
  #
1263
- # @return [Symbol] Representation of this class in an all-lowercase
1264
- # format, separated by underscores and demodulized.
1265
- def self.to_reference
1266
- name.to_s.
1267
- match(/^(?:.*::)*(.*)$/)[1].
1268
- gsub(/([a-z\d])([A-Z])/, '\1_\2').
1269
- downcase.to_sym
1270
- end
1271
-
1272
- # @private
1273
- def self.define_memoized_method(name, &block)
759
+ def self.counter(name)
1274
760
  define_method(name) do
1275
- @_memo[name] ||= instance_eval(&block)
761
+ return 0 if new?
762
+
763
+ key[:counters].hget(name).to_i
1276
764
  end
1277
765
  end
1278
766
 
1279
- # Allows you to find an {Ohm::Model} instance by its *id*.
1280
- #
1281
- # @param [#to_s] id The id of the model you want to find.
1282
- # @return [Ohm::Model, nil] The instance of Ohm::Model or nil of it does
1283
- # not exist.
1284
- def self.[](id)
1285
- new(:id => id) if id && exists?(id)
767
+ # An Ohm::Set wrapper for Model.key[:all].
768
+ def self.all
769
+ Set.new(key[:all], key, self)
1286
770
  end
1287
771
 
1288
- # @private Used for conveniently doing [1, 2].map(&Post) for example.
1289
- def self.to_proc
1290
- lambda { |id| new(:id => id) }
772
+ # Syntactic sugar for Model.new(atts).save
773
+ def self.create(atts = {})
774
+ new(atts).save
1291
775
  end
1292
776
 
1293
- # Returns a {Ohm::Model::Set set} containing all the members of a given
1294
- # class.
777
+ # Manipulate the Redis hash of attributes directly.
1295
778
  #
1296
- # @example
779
+ # Example:
1297
780
  #
1298
- # class Post < Ohm::Model
781
+ # class User < Ohm::Model
782
+ # attribute :name
1299
783
  # end
1300
784
  #
1301
- # post = Post.create
1302
- #
1303
- # Post.all.include?(post)
1304
- # # => true
785
+ # u = User.create(name: "John")
786
+ # u.key.hget(:name)
787
+ # # => John
1305
788
  #
1306
- # post.delete
789
+ # For more details see
790
+ # http://github.com/soveran/nest
1307
791
  #
1308
- # Post.all.include?(post)
1309
- # # => false
1310
- def self.all
1311
- Ohm::Model::Index.new(key[:all], Wrapper.wrap(self))
1312
- end
1313
-
1314
- # All the defined attributes within a class.
1315
- # @see Ohm::Model.attribute
1316
- def self.attributes
1317
- @@attributes[self]
1318
- end
1319
-
1320
- # All the defined counters within a class.
1321
- # @see Ohm::Model.counter
1322
- def self.counters
1323
- @@counters[self]
792
+ def key
793
+ model.key[id]
1324
794
  end
1325
795
 
1326
- # All the defined collections within a class. This will be comprised of
1327
- # all {Ohm::Model::Set sets} and {Ohm::Model::List lists} defined within
1328
- # your class.
796
+ # Initialize a model using a dictionary of attributes.
1329
797
  #
1330
- # @example
1331
- # class Post < Ohm::Model
1332
- # set :authors, Author
1333
- # list :comments, Comment
1334
- # end
798
+ # Example:
1335
799
  #
1336
- # Post.collections == [:authors, :comments]
1337
- # # => true
800
+ # u = User.new(name: "John")
1338
801
  #
1339
- # @see Ohm::Model.list
1340
- # @see Ohm::Model.set
1341
- def self.collections
1342
- @@collections[self]
1343
- end
1344
-
1345
- # All the defined indices within a class.
1346
- # @see Ohm::Model.index
1347
- def self.indices
1348
- @@indices[self]
802
+ def initialize(atts = {})
803
+ @attributes = {}
804
+ @_memo = {}
805
+ update_attributes(atts)
1349
806
  end
1350
807
 
1351
- # Convenience method to create and return the newly created object.
808
+ # Access the ID used to store this model. The ID is used together
809
+ # with the name of the class in order to form the Redis key.
1352
810
  #
1353
- # @example
811
+ # Example:
1354
812
  #
1355
- # class Post < Ohm::Model
1356
- # attribute :title
1357
- # end
813
+ # class User < Ohm::Model; end
1358
814
  #
1359
- # post = Post.create(:title => "A new post")
815
+ # u = User.create
816
+ # u.id
817
+ # # => 1
1360
818
  #
1361
- # @param [Hash] args attribute-value pairs for the object.
1362
- # @return [Ohm::Model] an instance of the class you're trying to create.
1363
- def self.create(*args)
1364
- model = new(*args)
1365
- model.create
1366
- model
819
+ # u.key
820
+ # # => User:1
821
+ #
822
+ def id
823
+ raise MissingID if not defined?(@id)
824
+ @id
1367
825
  end
1368
826
 
1369
- # Search across multiple indices and return the intersection of the sets.
827
+ # Check for equality by doing the following assertions:
1370
828
  #
1371
- # @example Finds all the user events for the supplied days
1372
- # event1 = Event.create day: "2009-09-09", author: "Albert"
1373
- # event2 = Event.create day: "2009-09-09", author: "Benoit"
1374
- # event3 = Event.create day: "2009-09-10", author: "Albert"
829
+ # 1. That the passed model is of the same type.
830
+ # 2. That they represent the same Redis key.
1375
831
  #
1376
- # [event1] == Event.find(author: "Albert", day: "2009-09-09").to_a
1377
- # # => true
1378
- def self.find(hash)
1379
- unless hash.kind_of?(Hash)
1380
- raise ArgumentError,
1381
- "You need to supply a hash with filters. " +
1382
- "If you want to find by ID, use #{self}[id] instead."
1383
- end
1384
-
1385
- all.find(hash)
832
+ def ==(other)
833
+ other.kind_of?(model) && other.key == key
834
+ rescue MissingID
835
+ false
1386
836
  end
1387
837
 
1388
- # Encode a value, making it safe to use as a key. Internally used by
1389
- # {Ohm::Model.index_key_for} to canonicalize the indexed values.
1390
- #
1391
- # @param [#to_s] value Any object you want to be able to use as a key.
1392
- # @return [String] A string which is safe to use as a key.
1393
- # @see Ohm::Model.index_key_for
1394
- def self.encode(value)
1395
- Base64.encode64(value.to_s).gsub("\n", "")
838
+ # Preload all the attributes of this model from Redis. Used
839
+ # internally by `Model::[]`.
840
+ def load!
841
+ update_attributes(key.hgetall) unless new?
842
+ return self
1396
843
  end
1397
844
 
1398
- # Constructor for all subclasses of {Ohm::Model}, which optionally
1399
- # takes a Hash of attribute value pairs.
1400
- #
1401
- # Starting with Ohm 0.1.0, you can use custom ids instead of being forced
1402
- # to use auto incrementing numeric ids, but keep in mind that you have
1403
- # to pass in the preferred id during object initialization.
1404
- #
1405
- # @example
1406
- #
1407
- # class User < Ohm::Model
1408
- # end
1409
- #
1410
- # class Post < Ohm::Model
1411
- # attribute :title
1412
- # reference :user, User
1413
- # end
845
+ # Read an attribute remotly from Redis. Useful if you want to get
846
+ # the most recent value of the attribute and not rely on locally
847
+ # cached value.
1414
848
  #
1415
- # user = User.create
1416
- # p1 = Post.new(:title => "Redis", :user_id => user.id)
1417
- # p1.save
849
+ # Example:
1418
850
  #
1419
- # p1.user_id == user.id
1420
- # # => true
851
+ # User.create(name: "A")
1421
852
  #
1422
- # p1.user == user
1423
- # # => true
853
+ # Session 1 | Session 2
854
+ # --------------|------------------------
855
+ # u = User[1] | u = User[1]
856
+ # u.name = "B" |
857
+ # u.save |
858
+ # | u.name == "A"
859
+ # | u.get(:name) == "B"
1424
860
  #
1425
- # # You can also just pass the actual User object, which is the better
1426
- # # way to do it:
1427
- # Post.new(:title => "Different way", :user => user).user == user
1428
- # # => true
861
+ def get(att)
862
+ @attributes[att] = key.hget(att)
863
+ end
864
+
865
+ # Update an attribute value atomically. The best usecase for this
866
+ # is when you simply want to update one value.
1429
867
  #
1430
- # # Let's try and generate custom ids
1431
- # p2 = Post.new(:id => "ohm-redis-library", :title => "Lib")
1432
- # p2 == Post["ohm-redis-library"]
1433
- # # => true
868
+ # Note: This method is dangerous because it doesn't update indices
869
+ # and uniques. Use it wisely. The safe equivalent is `update`.
1434
870
  #
1435
- # @param [Hash] attrs Attribute value pairs.
1436
- def initialize(attrs = {})
1437
- @id = nil
1438
- @_memo = {}
1439
- @_attributes = Hash.new { |hash, key| hash[key] = read_remote(key) }
1440
- update_attributes(attrs)
871
+ def set(att, val)
872
+ val.to_s.empty? ? key.hdel(att) : key.hset(att, val)
873
+ @attributes[att] = val
1441
874
  end
1442
875
 
1443
- # @return [true, false] Whether or not this object has an id.
1444
876
  def new?
1445
- !@id
877
+ !defined?(@id)
1446
878
  end
1447
879
 
1448
- # Create this model if it passes all validations.
1449
- #
1450
- # @return [Ohm::Model, nil] The newly created object or nil if it fails
1451
- # validation.
1452
- def create
1453
- return unless valid?
1454
- initialize_id
1455
-
1456
- mutex do
1457
- create_model_membership
1458
- write
1459
- add_to_indices
1460
- end
880
+ # Increment a counter atomically. Internally uses HINCRBY.
881
+ def incr(att, count = 1)
882
+ key[:counters].hincrby(att, count)
1461
883
  end
1462
884
 
1463
- # Create or update this object based on the state of #new?.
1464
- #
1465
- # @return [Ohm::Model, nil] The saved object or nil if it fails
1466
- # validation.
1467
- def save
1468
- return create if new?
1469
- return unless valid?
1470
-
1471
- mutex do
1472
- write
1473
- update_indices
1474
- end
885
+ # Decrement a counter atomically. Internally uses HINCRBY.
886
+ def decr(att, count = 1)
887
+ incr(att, -count)
1475
888
  end
1476
889
 
1477
- # Update this object, optionally accepting new attributes.
890
+ # Return a value that allows the use of models as hash keys.
1478
891
  #
1479
- # @param [Hash] attrs Attribute value pairs to use for the updated
1480
- # version
1481
- # @return [Ohm::Model, nil] The updated object or nil if it fails
1482
- # validation.
1483
- def update(attrs)
1484
- update_attributes(attrs)
1485
- save
1486
- end
1487
-
1488
- # Locally update all attributes without persisting the changes.
1489
- # Internally used by {Ohm::Model#initialize} and {Ohm::Model#update}
1490
- # to set attribute value pairs.
892
+ # Example:
1491
893
  #
1492
- # @param [Hash] attrs Attribute value pairs.
1493
- def update_attributes(attrs)
1494
- attrs.each do |key, value|
1495
- send(:"#{key}=", value)
1496
- end
1497
- end
1498
-
1499
- # Delete this object from the _Redis_ datastore, ensuring that all
1500
- # indices, attributes, collections, etc are also deleted with it.
894
+ # h = {}
1501
895
  #
1502
- # @return [Ohm::Model] Returns a reference of itself.
1503
- def delete
1504
- delete_from_indices
1505
- delete_attributes(collections) unless collections.empty?
1506
- delete_model_membership
1507
- self
1508
- end
1509
-
1510
- # Increment the counter denoted by :att.
896
+ # u = User.new
1511
897
  #
1512
- # @param [Symbol] att Attribute to increment.
1513
- # @param [Fixnum] count An optional increment step to use.
1514
- def incr(att, count = 1)
1515
- unless counters.include?(att)
1516
- raise ArgumentError, "#{att.inspect} is not a counter."
1517
- end
1518
-
1519
- write_local(att, key.hincrby(att, count))
898
+ # h[:u] = u
899
+ # h[:u] == u
900
+ # # => true
901
+ #
902
+ def hash
903
+ new? ? super : key.hash
1520
904
  end
905
+ alias :eql? :==
1521
906
 
1522
- # Decrement the counter denoted by :att.
1523
- #
1524
- # @param [Symbol] att Attribute to decrement.
1525
- # @param [Fixnum] count An optional decrement step to use.
1526
- def decr(att, count = 1)
1527
- incr(att, -count)
907
+ def attributes
908
+ @attributes
1528
909
  end
1529
910
 
1530
- # Export the id and errors of the object. The `to_hash` takes the opposite
1531
- # approach of providing all the attributes and instead favors a white
1532
- # listed approach.
911
+ # Export the ID and the errors of the model. The approach of Ohm
912
+ # is to whitelist public attributes, as opposed to exporting each
913
+ # (possibly sensitive) attribute.
1533
914
  #
1534
- # @example
915
+ # Example:
1535
916
  #
1536
- # person = Person.create(:name => "John Doe")
1537
- # person.to_hash == { :id => '1' }
1538
- # # => true
917
+ # class User < Ohm::Model
918
+ # attribute :name
919
+ # end
1539
920
  #
1540
- # # if the person asserts presence of name, the errors will be included
1541
- # person = Person.create(:name => "John Doe")
1542
- # person.name = nil
1543
- # person.valid?
1544
- # # => false
921
+ # u = User.create(name: "John")
922
+ # u.to_hash
923
+ # # => { id: "1" }
1545
924
  #
1546
- # person.to_hash == { :id => '1', :errors => [[:name, :not_present]] }
1547
- # # => true
925
+ # In order to add additional attributes, you can override `to_hash`:
1548
926
  #
1549
- # # for cases where you want to provide white listed attributes just do:
927
+ # class User < Ohm::Model
928
+ # attribute :name
1550
929
  #
1551
- # class Person < Ohm::Model
1552
930
  # def to_hash
1553
- # super.merge(:name => name)
931
+ # super.merge(name: name)
1554
932
  # end
1555
933
  # end
1556
934
  #
1557
- # # now we have the name when doing a to_hash
1558
- # person = Person.create(:name => "John Doe")
1559
- # person.to_hash == { :id => '1', :name => "John Doe" }
1560
- # # => true
935
+ # u = User.create(name: "John")
936
+ # u.to_hash
937
+ # # => { id: "1", name: "John" }
938
+ #
1561
939
  def to_hash
1562
940
  attrs = {}
1563
941
  attrs[:id] = id unless new?
1564
- attrs[:errors] = errors unless errors.empty?
1565
- attrs
942
+ attrs[:errors] = errors if errors.any?
943
+
944
+ return attrs
945
+ end
946
+
947
+ # Export a JSON representation of the model by encoding `to_hash`.
948
+ def to_json(*args)
949
+ to_hash.to_json(*args)
1566
950
  end
1567
951
 
1568
- # Returns the JSON representation of the {#to_hash} for this object.
1569
- # Defining a custom {#to_hash} method will also affect this and return
1570
- # a corresponding JSON representation of whatever you have in your
1571
- # {#to_hash}.
952
+ # Persist the model attributes and update indices and unique
953
+ # indices. The `counter`s and `set`s are not touched during save.
1572
954
  #
1573
- # @example
1574
- # require "json"
955
+ # If the model is not valid, nil is returned. Otherwise, the
956
+ # persisted model is returned.
1575
957
  #
1576
- # class Post < Ohm::Model
1577
- # attribute :title
958
+ # Example:
1578
959
  #
1579
- # def to_hash
1580
- # super.merge(:title => title)
960
+ # class User < Ohm::Model
961
+ # attribute :name
962
+ #
963
+ # def validate
964
+ # assert_present :name
1581
965
  # end
1582
966
  # end
1583
967
  #
1584
- # p1 = Post.create(:title => "Delta Force")
1585
- # p1.to_hash == { :id => "1", :title => "Delta Force" }
1586
- # # => true
968
+ # User.new(name: nil).save
969
+ # # => nil
1587
970
  #
1588
- # p1.to_json == "{\"id\":\"1\",\"title\":\"Delta Force\"}"
971
+ # u = User.new(name: "John").save
972
+ # u.kind_of?(User)
1589
973
  # # => true
1590
974
  #
1591
- # @return [String] The JSON representation of this object defined in
1592
- # terms of {#to_hash}.
1593
- def to_json(*args)
1594
- to_hash.to_json(*args)
975
+ def save(&block)
976
+ return if not valid?
977
+ save!(&block)
1595
978
  end
1596
979
 
1597
- # Convenience wrapper for {Ohm::Model.attributes}.
1598
- def attributes
1599
- self.class.attributes
1600
- end
980
+ # Saves the model without checking for validity. Refer to
981
+ # `Model#save` for more details.
982
+ def save!
983
+ transaction do |t|
984
+ t.watch(*_unique_keys)
985
+ t.watch(key) if not new?
1601
986
 
1602
- # Convenience wrapper for {Ohm::Model.counters}.
1603
- def counters
1604
- self.class.counters
1605
- end
987
+ t.before do
988
+ _initialize_id if new?
989
+ end
1606
990
 
1607
- # Convenience wrapper for {Ohm::Model.collections}.
1608
- def collections
1609
- self.class.collections
1610
- end
991
+ t.read do |store|
992
+ _verify_uniques
993
+ store.existing = key.hgetall
994
+ store.uniques = _read_index_type(:uniques)
995
+ store.indices = _read_index_type(:indices)
996
+ end
1611
997
 
1612
- # Convenience wrapper for {Ohm::Model.indices}.
1613
- def indices
1614
- self.class.indices
1615
- end
998
+ t.write do |store|
999
+ model.key[:all].sadd(id)
1000
+ _delete_uniques(store.existing)
1001
+ _delete_indices(store.existing)
1002
+ _save
1003
+ _save_indices(store.indices)
1004
+ _save_uniques(store.uniques)
1005
+ end
1616
1006
 
1617
- # Implementation of equality checking. Equality is defined by two simple
1618
- # rules:
1619
- #
1620
- # 1. They have the same class.
1621
- # 2. They have the same key (_Redis_ key e.g. Post:1 == Post:1).
1622
- #
1623
- # @return [true, false] Whether or not the passed object is equal.
1624
- def ==(other)
1625
- other.kind_of?(self.class) && other.key == key
1626
- rescue MissingID
1627
- false
1628
- end
1629
- alias :eql? :==
1007
+ yield t if block_given?
1008
+ end
1630
1009
 
1631
- # Allows you to safely use an instance of {Ohm::Model} as a key in a
1632
- # Ruby hash without running into weird scenarios.
1633
- #
1634
- # @example
1635
- #
1636
- # class Post < Ohm::Model
1637
- # end
1638
- #
1639
- # h = {}
1640
- # p1 = Post.new
1641
- # h[p1] = "Ruby"
1642
- # h[p1] == "Ruby"
1643
- # # => true
1644
- #
1645
- # p1.save
1646
- # h[p1] == "Ruby"
1647
- # # => false
1648
- #
1649
- # @return [Fixnum] An integer representing this object to be used
1650
- # as the index for hashes in Ruby.
1651
- def hash
1652
- new? ? super : key.hash
1010
+ return self
1653
1011
  end
1654
1012
 
1655
- # Lock the object before executing the block, and release it once the
1656
- # block is done.
1013
+ # Delete the model, including all the following keys:
1657
1014
  #
1658
- # This is used during {#create} and {#save} to ensure that no race
1659
- # conditions occur.
1015
+ # - <Model>:<id>
1016
+ # - <Model>:<id>:counters
1017
+ # - <Model>:<id>:<set name>
1660
1018
  #
1661
- # @see http://code.google.com/p/redis/wiki/SetnxCommand SETNX in the
1662
- # Redis Command Reference.
1663
- def mutex
1664
- lock!
1665
- yield
1666
- self
1667
- ensure
1668
- unlock!
1669
- end
1670
-
1671
- # Returns everything, including {Ohm::Model.attributes attributes},
1672
- # {Ohm::Model.collections collections}, {Ohm::Model.counters counters},
1673
- # and the id of this object.
1019
+ # If the model has uniques or indices, they're also cleaned up.
1674
1020
  #
1675
- # Useful for debugging and for doing irb work.
1676
- def inspect
1677
- everything = (attributes + collections + counters).map do |att|
1678
- value = begin
1679
- send(att)
1680
- rescue MissingID
1681
- nil
1682
- end
1021
+ def delete
1022
+ transaction do |t|
1023
+ t.read do |store|
1024
+ store.existing = key.hgetall
1025
+ end
1683
1026
 
1684
- [att, value.inspect]
1685
- end
1027
+ t.write do |store|
1028
+ _delete_uniques(store.existing)
1029
+ _delete_indices(store.existing)
1030
+ model.collections.each { |e| key[e].del }
1031
+ model.key[:all].srem(id)
1032
+ key[:counters].del
1033
+ key.del
1034
+ end
1686
1035
 
1687
- sprintf("#<%s:%s %s>",
1688
- self.class,
1689
- new? ? "?" : id,
1690
- everything.map {|e| e.join("=") }.join(" ")
1691
- )
1036
+ yield t if block_given?
1037
+ end
1692
1038
  end
1693
1039
 
1694
- # Makes the model connect to a different Redis instance. This is useful
1695
- # for scaling a large application, where one model can be stored in a
1696
- # different Redis instance, and some other groups of models can be
1697
- # in another Redis instance.
1040
+ # Update the model attributes and call save.
1698
1041
  #
1699
- # This approach of splitting models is a lot simpler than doing a
1700
- # distributed *Redis* solution and may well be the right solution for
1701
- # certain cases.
1042
+ # Example:
1702
1043
  #
1703
- # @example
1704
- #
1705
- # class Post < Ohm::Model
1706
- # connect :port => 6380, :db => 2
1044
+ # User[1].update(name: "John")
1707
1045
  #
1708
- # attribute :body
1709
- # end
1046
+ # # It's the same as:
1710
1047
  #
1711
- # # Since these settings are usually environment-specific,
1712
- # # you may want to call this method from outside of the class
1713
- # # definition:
1714
- # Post.connect(:port => 6380, :db => 2)
1048
+ # u = User[1]
1049
+ # u.update_attributes(name: "John")
1050
+ # u.save
1715
1051
  #
1716
- # @see file:README.html#connecting Ohm.connect options documentation.
1717
- def self.connect(options = {})
1718
- Ohm.threaded[self] = nil
1719
- @options = options
1052
+ def update(attributes)
1053
+ update_attributes(attributes)
1054
+ save
1720
1055
  end
1721
1056
 
1722
- # @return [Ohm::Key] A key scoped to the model which uses this object's
1723
- # id.
1724
- #
1725
- # @see http://github.com/soveran/nest The Nest library.
1726
- def key
1727
- self.class.key[id]
1057
+ # Write the dictionary of key-value pairs to the model.
1058
+ def update_attributes(atts)
1059
+ atts.each { |att, val| send(:"#{att}=", val) }
1728
1060
  end
1729
1061
 
1730
1062
  protected
1731
- attr_writer :id
1732
-
1733
-
1734
- # Write all the attributes and counters of this object. The operation
1735
- # is actually a 2-step process:
1736
- #
1737
- # 1. Delete the current key, e.g. Post:2.
1738
- # 2. Set all of the new attributes (using HMSET).
1739
- #
1740
- # The DEL and HMSET operations are wrapped in a MULTI EXEC block to ensure
1741
- # the atomicity of the write operation.
1742
- #
1743
- # @see http://code.google.com/p/redis/wiki/DelCommand DEL in the
1744
- # Redis Command Reference.
1745
- # @see http://code.google.com/p/redis/wiki/HmsetCommand HMSET in the
1746
- # Redis Command Reference.
1747
- # @see http://code.google.com/p/redis/wiki/MultiExecCommand MULTI EXEC
1748
- # in the Redis Command Reference.
1749
- def write
1750
- unless (attributes + counters).empty?
1751
- atts = (attributes + counters).inject([]) { |ret, att|
1752
- value = send(att).to_s
1753
-
1754
- ret.push(att, value) if not value.empty?
1755
- ret
1756
- }
1757
-
1758
- db.multi do
1759
- key.del
1760
- key.hmset(*atts.flatten) if atts.any?
1761
- end
1762
- end
1063
+ def self.to_reference
1064
+ name.to_s.
1065
+ match(/^(?:.*::)*(.*)$/)[1].
1066
+ gsub(/([a-z\d])([A-Z])/, '\1_\2').
1067
+ downcase.to_sym
1763
1068
  end
1764
1069
 
1765
- # Write a single attribute both locally and remotely. It's very important
1766
- # to know that this method skips validation checks, therefore you must
1767
- # ensure data integrity and validity in your application code.
1768
- #
1769
- # @param [Symbol, String] att The name of the attribute to write.
1770
- # @param [#to_s] value The value of the attribute to write.
1771
- #
1772
- # @see http://code.google.com/p/redis/wiki/HdelCommand HDEL in the
1773
- # Redis Command Reference.
1774
- # @see http://code.google.com/p/redis/wiki/HsetCommand HSET in the
1775
- # Redis Command Reference.
1776
- def write_remote(att, value)
1777
- write_local(att, value)
1070
+ def self.indices
1071
+ @indices ||= []
1072
+ end
1778
1073
 
1779
- if value.to_s.empty?
1780
- key.hdel(att)
1781
- else
1782
- key.hset(att, value)
1783
- end
1074
+ def self.uniques
1075
+ @uniques ||= []
1784
1076
  end
1785
1077
 
1786
- # Wraps any missing constants lazily in {Ohm::Model::Wrapper} delaying
1787
- # the evaluation of constants until they are actually needed.
1788
- #
1789
- # @see Ohm::Model::Wrapper
1790
- # @see http://en.wikipedia.org/wiki/Lazy_evaluation Lazy evaluation
1791
- def self.const_missing(name)
1792
- wrapper = Wrapper.new(name) { const_get(name) }
1078
+ def self.collections
1079
+ @collections ||= []
1080
+ end
1793
1081
 
1794
- # Allow others to hook to const_missing.
1795
- begin
1796
- super(name)
1797
- rescue NameError
1082
+ def self.filters(dict)
1083
+ unless dict.kind_of?(Hash)
1084
+ raise ArgumentError,
1085
+ "You need to supply a hash with filters. " +
1086
+ "If you want to find by ID, use #{self}[id] instead."
1798
1087
  end
1799
1088
 
1800
- wrapper
1089
+ dict.map { |k, v| toindices(k, v) }.flatten
1801
1090
  end
1802
1091
 
1803
- private
1804
-
1805
- # Provides access to the Redis database. This is shared accross all models and instances.
1806
- def self.db
1807
- return Ohm.redis unless defined?(@options)
1092
+ def self.toindices(att, val)
1093
+ raise IndexNotFound unless indices.include?(att)
1808
1094
 
1809
- Ohm.threaded[self] ||= Redis.connect(@options)
1095
+ if val.kind_of?(Enumerable)
1096
+ val.map { |v| key[:indices][att][v] }
1097
+ else
1098
+ [key[:indices][att][val]]
1099
+ end
1810
1100
  end
1811
1101
 
1812
- # Allows you to do key manipulations scoped solely to your class.
1813
- def self.key
1814
- Key.new(self, db)
1102
+ def self.new_id
1103
+ key[:id].incr
1815
1104
  end
1816
1105
 
1817
- def self.exists?(id)
1818
- key[:all].sismember(id)
1106
+ attr_writer :id
1107
+
1108
+ def transaction
1109
+ txn = Transaction.new { |t| yield t }
1110
+ txn.commit(db)
1819
1111
  end
1820
1112
 
1821
- # The meat of the ID generation code for Ohm. For cases where you want to
1822
- # customize ID generation (i.e. use GUIDs or Base62 ids) then you simply
1823
- # override this method in your model.
1824
- #
1825
- # @example
1826
- #
1827
- # module UUID
1828
- # def self.new
1829
- # `uuidgen`.strip
1830
- # end
1831
- # end
1832
- #
1833
- # class Post < Ohm::Model
1834
- #
1835
- # private
1836
- # def initialize_id
1837
- # @id ||= UUID.new
1838
- # end
1839
- # end
1840
- #
1841
- def initialize_id
1842
- @id ||= self.class.key[:id].incr.to_s
1113
+ def model
1114
+ self.class
1843
1115
  end
1844
1116
 
1845
1117
  def db
1846
- self.class.db
1118
+ model.db
1847
1119
  end
1848
1120
 
1849
- def delete_attributes(atts)
1850
- db.del(*atts.map { |att| key[att] })
1121
+ def _initialize_id
1122
+ @id = model.new_id.to_s
1851
1123
  end
1852
1124
 
1853
- def create_model_membership
1854
- self.class.all << self
1125
+ def _skip_empty(atts)
1126
+ {}.tap do |ret|
1127
+ atts.each do |att, val|
1128
+ ret[att] = send(att).to_s unless val.to_s.empty?
1129
+ end
1130
+ end
1855
1131
  end
1856
1132
 
1857
- def delete_model_membership
1858
- key.del
1859
- self.class.all.delete(self)
1133
+ def _unique_keys
1134
+ model.uniques.map { |att| model.key[:uniques][att] }
1860
1135
  end
1861
1136
 
1862
- def update_indices
1863
- delete_from_indices
1864
- add_to_indices
1137
+ def _save
1138
+ key.del
1139
+ key.hmset(*_skip_empty(attributes).flatten)
1865
1140
  end
1866
1141
 
1867
- def add_to_indices
1868
- indices.each do |att|
1869
- next add_to_index(att) unless collection?(send(att))
1870
- send(att).each { |value| add_to_index(att, value) }
1142
+ def _verify_uniques
1143
+ if att = _detect_duplicate
1144
+ raise UniqueIndexViolation, "#{att} is not unique."
1871
1145
  end
1872
1146
  end
1873
1147
 
1874
- def collection?(value)
1875
- self.class.collection?(value)
1876
- end
1877
-
1878
- def self.collection?(value)
1879
- value.kind_of?(Enumerable) &&
1880
- value.kind_of?(String) == false
1148
+ def _detect_duplicate
1149
+ model.uniques.detect do |att|
1150
+ id = model.key[:uniques][att].hget(send(att))
1151
+ id && id != self.id.to_s
1152
+ end
1881
1153
  end
1882
1154
 
1883
- def add_to_index(att, value = send(att))
1884
- index = index_key_for(att, value)
1885
- index.sadd(id)
1886
- key[:_indices].sadd(index)
1155
+ def _read_index_type(type)
1156
+ {}.tap do |ret|
1157
+ model.send(type).each do |att|
1158
+ ret[att] = send(att)
1159
+ end
1160
+ end
1887
1161
  end
1888
1162
 
1889
- def delete_from_indices
1890
- key[:_indices].smembers.each do |index|
1891
- db.srem(index, id)
1163
+ def _save_uniques(uniques)
1164
+ uniques.each do |att, val|
1165
+ model.key[:uniques][att].hset(val, id)
1892
1166
  end
1893
-
1894
- key[:_indices].del
1895
1167
  end
1896
1168
 
1897
- # Get the value of a specific attribute. An important fact about
1898
- # attributes in Ohm is that they are all loaded lazily.
1899
- #
1900
- # @param [Symbol] att The attribute you you want to get.
1901
- # @return [String] The value of att.
1902
- def read_local(att)
1903
- @_attributes[att]
1169
+ def _delete_uniques(atts)
1170
+ model.uniques.each do |att|
1171
+ model.key[:uniques][att].hdel(atts[att.to_s])
1172
+ end
1904
1173
  end
1905
1174
 
1906
- # Write the value of an attribute locally, without persisting it.
1907
- #
1908
- # @param [Symbol] att The attribute you want to set.
1909
- # @param [#to_s] value The value of the attribute you want to set.
1910
- def write_local(att, value)
1911
- @_attributes[att] = value
1912
- end
1175
+ def _delete_indices(atts)
1176
+ model.indices.each do |att|
1177
+ val = atts[att.to_s]
1913
1178
 
1914
- # Used internally be the @_attributes hash to lazily load attributes
1915
- # when you need them. You may also use this in your code if you know what
1916
- # you are doing.
1917
- #
1918
- # @param [Symbol] att The attribute you you want to get.
1919
- # @return [String] The value of att.
1920
- def read_remote(att)
1921
- unless new?
1922
- value = key.hget(att)
1923
- value.respond_to?(:force_encoding) ?
1924
- value.force_encoding("UTF-8") :
1925
- value
1179
+ if val
1180
+ model.key[:indices][att][val].srem(id)
1181
+ end
1926
1182
  end
1927
1183
  end
1928
1184
 
1929
- # Read attributes en masse locally.
1930
- def read_locals(attrs)
1931
- attrs.map do |att|
1932
- send(att)
1185
+ def _save_indices(indices)
1186
+ indices.each do |att, val|
1187
+ model.toindices(att, val).each do |index|
1188
+ index.sadd(id)
1189
+ end
1933
1190
  end
1934
1191
  end
1192
+ end
1935
1193
 
1936
- # Read attributes en masse remotely.
1937
- def read_remotes(attrs)
1938
- attrs.map do |att|
1939
- read_remote(att)
1940
- end
1941
- end
1194
+ class Lua
1195
+ attr :dir
1196
+ attr :redis
1197
+ attr :files
1198
+ attr :scripts
1942
1199
 
1943
- # Get the index name for a specific index and value pair. The return value
1944
- # is an instance of {Ohm::Key}, which you can readily do Redis operations
1945
- # on.
1946
- #
1947
- # @example
1948
- #
1949
- # class Post < Ohm::Model
1950
- # attribute :title
1951
- # index :title
1952
- # end
1953
- #
1954
- # post = Post.create(:title => "Foo")
1955
- # key = Post.index_key_for(:title, "Foo")
1956
- # key == "Post:title:Rm9v"
1957
- # key.scard == 1
1958
- # key.smembers == [post.id]
1959
- # # => true
1960
- #
1961
- # @param [Symbol] name The name of the index.
1962
- # @param [#to_s] value The value for the index.
1963
- # @return [Ohm::Key] A {Ohm::Key key} which you can treat as a string,
1964
- # but also do Redis operations on.
1965
- def self.index_key_for(name, value)
1966
- raise IndexNotFound, name unless indices.include?(name)
1967
- key[name][encode(value)]
1200
+ def initialize(dir, redis)
1201
+ @dir = dir
1202
+ @redis = redis
1203
+ @files = Hash.new { |h, cmd| h[cmd] = read(cmd) }
1204
+ @scripts = {}
1968
1205
  end
1969
1206
 
1970
- # Thin wrapper around {Ohm::Model.index_key_for}.
1971
- def index_key_for(att, value)
1972
- self.class.index_key_for(att, value)
1207
+ def run_file(file, options)
1208
+ run(files[file], options)
1973
1209
  end
1974
1210
 
1975
- # Lock the object so no other instances can modify it.
1976
- # This method implements the design pattern for locks
1977
- # described at: http://code.google.com/p/redis/wiki/SetnxCommand
1978
- #
1979
- # @see Model#mutex
1980
- def lock!
1981
- until key[:_lock].setnx(Time.now.to_f + 0.5)
1982
- next unless timestamp = key[:_lock].get
1983
- sleep(0.1) and next unless lock_expired?(timestamp)
1211
+ def run(script, options)
1212
+ keys = options[:keys]
1213
+ argv = options[:argv]
1984
1214
 
1985
- break unless timestamp = key[:_lock].getset(Time.now.to_f + 0.5)
1986
- break if lock_expired?(timestamp)
1215
+ begin
1216
+ redis.evalsha(sha(script), keys.size, *keys, *argv)
1217
+ rescue RuntimeError
1218
+ redis.eval(script, keys.size, *keys, *argv)
1987
1219
  end
1988
1220
  end
1989
1221
 
1990
- # Release the lock.
1991
- # @see Model#mutex
1992
- def unlock!
1993
- key[:_lock].del
1222
+ private
1223
+ def read(file)
1224
+ File.read("%s/%s.lua" % [dir, file])
1994
1225
  end
1995
1226
 
1996
- def lock_expired?(timestamp)
1997
- timestamp.to_f < Time.now.to_f
1227
+ def sha(script)
1228
+ Digest::SHA1.hexdigest(script)
1998
1229
  end
1999
1230
  end
2000
1231
  end