ohm 0.1.5 → 1.0.0.alpha1

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/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