mongo_mapper 0.7.0 → 0.7.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. data/README.rdoc +3 -1
  2. data/Rakefile +9 -12
  3. data/lib/mongo_mapper.rb +30 -10
  4. data/lib/mongo_mapper/document.rb +16 -74
  5. data/lib/mongo_mapper/embedded_document.rb +7 -1
  6. data/lib/mongo_mapper/plugins.rb +3 -0
  7. data/lib/mongo_mapper/plugins/associations/embedded_collection.rb +1 -12
  8. data/lib/mongo_mapper/plugins/associations/in_array_proxy.rb +6 -1
  9. data/lib/mongo_mapper/plugins/associations/many_documents_proxy.rb +4 -1
  10. data/lib/mongo_mapper/plugins/callbacks.rb +183 -12
  11. data/lib/mongo_mapper/plugins/keys.rb +17 -5
  12. data/lib/mongo_mapper/plugins/modifiers.rb +87 -0
  13. data/lib/mongo_mapper/plugins/pagination/proxy.rb +7 -3
  14. data/lib/mongo_mapper/plugins/protected.rb +1 -1
  15. data/lib/mongo_mapper/plugins/rails.rb +16 -8
  16. data/lib/mongo_mapper/plugins/serialization.rb +51 -81
  17. data/lib/mongo_mapper/plugins/timestamps.rb +21 -0
  18. data/lib/mongo_mapper/plugins/userstamps.rb +14 -0
  19. data/lib/mongo_mapper/query.rb +1 -1
  20. data/lib/mongo_mapper/version.rb +3 -0
  21. data/mongo_mapper.gemspec +22 -11
  22. data/test/active_model_lint_test.rb +11 -0
  23. data/test/functional/associations/test_in_array_proxy.rb +16 -0
  24. data/test/functional/associations/test_many_documents_proxy.rb +22 -0
  25. data/test/functional/test_callbacks.rb +104 -34
  26. data/test/functional/test_document.rb +70 -149
  27. data/test/functional/test_embedded_document.rb +39 -34
  28. data/test/functional/test_indexing.rb +44 -0
  29. data/test/functional/test_modifiers.rb +297 -227
  30. data/test/functional/test_protected.rb +11 -5
  31. data/test/functional/test_timestamps.rb +64 -0
  32. data/test/functional/test_userstamps.rb +28 -0
  33. data/test/support/timing.rb +1 -1
  34. data/test/unit/serializers/test_json_serializer.rb +30 -17
  35. data/test/unit/test_embedded_document.rb +15 -15
  36. data/test/unit/test_keys.rb +15 -11
  37. data/test/unit/test_mongo_mapper.rb +31 -1
  38. data/test/unit/test_pagination.rb +33 -0
  39. data/test/unit/test_query.rb +6 -0
  40. data/test/unit/test_serialization.rb +3 -3
  41. data/test/unit/test_support.rb +9 -5
  42. metadata +17 -6
  43. data/VERSION +0 -1
@@ -1,6 +1,6 @@
1
1
  = MongoMapper
2
2
 
3
- Awesome gem for modeling your domain and storing it in mongo.
3
+ A Ruby Object Mapper for Mongo.
4
4
 
5
5
  Releases are tagged on github and released on gemcutter. Master is pushed to whenever I add a patch or a new feature, but I do not release a new gem version each time I push.
6
6
 
@@ -24,6 +24,8 @@ http://groups.google.com/group/mongomapper
24
24
  To see if the problem you are having is a verified issue, you can see the MM pivotal tracker project:
25
25
  http://www.pivotaltracker.com/projects/33576
26
26
 
27
+ There is no need to request to join the Pivotal Tracker project as I am only granting access to a select few (easier to keep things organized). If you have a problem, please use the mailing list. If I confirm it to be a bug, I am happy to add it to PT. Thanks!
28
+
27
29
  == Copyright
28
30
 
29
31
  Copyright (c) 2009 John Nunemaker. See LICENSE for details.
data/Rakefile CHANGED
@@ -1,20 +1,21 @@
1
1
  require 'rubygems'
2
2
  require 'rake'
3
3
  require 'jeweler'
4
- require 'yard'
5
- require 'yard/rake/yardoc_task'
4
+
5
+ require File.dirname(__FILE__) + '/lib/mongo_mapper/version'
6
6
 
7
7
  Jeweler::Tasks.new do |gem|
8
8
  gem.name = "mongo_mapper"
9
- gem.summary = %Q{Awesome gem for modeling your domain and storing it in mongo}
9
+ gem.summary = %Q{A Ruby Object Mapper for Mongo}
10
10
  gem.email = "nunemaker@gmail.com"
11
11
  gem.homepage = "http://github.com/jnunemaker/mongomapper"
12
12
  gem.authors = ["John Nunemaker"]
13
-
13
+ gem.version = MongoMapper::Version
14
+
14
15
  gem.add_dependency('activesupport', '>= 2.3')
15
- gem.add_dependency('mongo', '0.18.3')
16
- gem.add_dependency('jnunemaker-validatable', '1.8.1')
17
-
16
+ gem.add_dependency('mongo', '0.19.1')
17
+ gem.add_dependency('jnunemaker-validatable', '1.8.3')
18
+
18
19
  gem.add_development_dependency('jnunemaker-matchy', '0.4.0')
19
20
  gem.add_development_dependency('shoulda', '2.10.2')
20
21
  gem.add_development_dependency('timecop', '0.3.1')
@@ -38,7 +39,7 @@ namespace :test do
38
39
  test.pattern = 'test/unit/**/test_*.rb'
39
40
  test.verbose = true
40
41
  end
41
-
42
+
42
43
  Rake::TestTask.new(:functionals) do |test|
43
44
  test.libs << 'test'
44
45
  test.ruby_opts << '-rubygems'
@@ -49,7 +50,3 @@ end
49
50
 
50
51
  task :default => :test
51
52
  task :test => :check_dependencies
52
-
53
- YARD::Rake::YardocTask.new(:doc) do |t|
54
- t.options = ["--legacy"] if RUBY_VERSION < "1.9.0"
55
- end
@@ -1,4 +1,5 @@
1
1
  require 'set'
2
+ require 'uri'
2
3
 
3
4
  # if Gem is defined i'll assume you are using rubygems and lock specific versions
4
5
  # call me crazy but a plain old require will just get the latest version you have installed
@@ -6,11 +7,11 @@ require 'set'
6
7
  # if there is a better way to do this, please enlighten me!
7
8
  if self.class.const_defined?(:Gem)
8
9
  gem 'activesupport', '>= 2.3'
9
- gem 'mongo', '0.18.3'
10
- gem 'jnunemaker-validatable', '1.8.1'
10
+ gem 'mongo', '0.19.1'
11
+ gem 'jnunemaker-validatable', '1.8.3'
11
12
  end
12
13
 
13
- require 'active_support'
14
+ require 'active_support/all'
14
15
  require 'mongo'
15
16
  require 'validatable'
16
17
 
@@ -24,6 +25,9 @@ module MongoMapper
24
25
  # raised when document expected but not found
25
26
  class DocumentNotFound < MongoMapperError; end
26
27
 
28
+ # raised when trying to connect using uri with incorrect scheme
29
+ class InvalidScheme < MongoMapperError; end
30
+
27
31
  # raised when document not valid and using !
28
32
  class DocumentNotValid < MongoMapperError
29
33
  def initialize(document)
@@ -70,13 +74,28 @@ module MongoMapper
70
74
  @@config
71
75
  end
72
76
 
77
+ # @api private
78
+ def self.config_for_environment(environment)
79
+ env = config[environment]
80
+ return env if env['uri'].blank?
81
+
82
+ uri = URI.parse(env['uri'])
83
+ raise InvalidScheme.new('must be mongodb') unless uri.scheme == 'mongodb'
84
+ {
85
+ 'host' => uri.host,
86
+ 'port' => uri.port,
87
+ 'database' => uri.path.gsub(/^\//, ''),
88
+ 'username' => uri.user,
89
+ 'password' => uri.password,
90
+ }
91
+ end
92
+
73
93
  def self.connect(environment, options={})
74
94
  raise 'Set config before connecting. MongoMapper.config = {...}' if config.blank?
75
- MongoMapper.connection = Mongo::Connection.new(config[environment]['host'], config[environment]['port'], options)
76
- MongoMapper.database = config[environment]['database']
77
- if config[environment]['username'].present? && config[environment]['password'].present?
78
- MongoMapper.database.authenticate(config[environment]['username'], config[environment]['password'])
79
- end
95
+ env = config_for_environment(environment)
96
+ MongoMapper.connection = Mongo::Connection.new(env['host'], env['port'], options)
97
+ MongoMapper.database = env['database']
98
+ MongoMapper.database.authenticate(env['username'], env['password']) if env['username'] && env['password']
80
99
  end
81
100
 
82
101
  def self.setup(config, environment, options={})
@@ -108,11 +127,12 @@ module MongoMapper
108
127
  def self.normalize_object_id(value)
109
128
  value.is_a?(String) ? Mongo::ObjectID.from_string(value) : value
110
129
  end
111
-
130
+
112
131
  autoload :Query, 'mongo_mapper/query'
113
132
  autoload :Document, 'mongo_mapper/document'
114
133
  autoload :EmbeddedDocument, 'mongo_mapper/embedded_document'
134
+ autoload :Version, 'mongo_mapper/version'
115
135
  end
116
136
 
117
137
  require 'mongo_mapper/support'
118
- require 'mongo_mapper/plugins'
138
+ require 'mongo_mapper/plugins'
@@ -17,13 +17,16 @@ module MongoMapper
17
17
  plugin Plugins::Keys
18
18
  plugin Plugins::Dirty # for now dirty needs to be after keys
19
19
  plugin Plugins::Logger
20
+ plugin Plugins::Modifiers
20
21
  plugin Plugins::Pagination
21
22
  plugin Plugins::Protected
22
23
  plugin Plugins::Rails
23
24
  plugin Plugins::Serialization
25
+ plugin Plugins::Timestamps
26
+ plugin Plugins::Userstamps
24
27
  plugin Plugins::Validations
25
28
  plugin Plugins::Callbacks # for now callbacks needs to be after validations
26
-
29
+
27
30
  extend Plugins::Validations::DocumentMacros
28
31
  end
29
32
 
@@ -143,47 +146,6 @@ module MongoMapper
143
146
  find_each(options) { |document| document.destroy }
144
147
  end
145
148
 
146
- def increment(*args)
147
- modifier_update('$inc', args)
148
- end
149
-
150
- def decrement(*args)
151
- criteria, keys = criteria_and_keys_from_args(args)
152
- values, to_decrement = keys.values, {}
153
- keys.keys.each_with_index { |k, i| to_decrement[k] = -values[i].abs }
154
- collection.update(criteria, {'$inc' => to_decrement}, :multi => true)
155
- end
156
-
157
- def set(*args)
158
- modifier_update('$set', args)
159
- end
160
-
161
- def push(*args)
162
- modifier_update('$push', args)
163
- end
164
-
165
- def push_all(*args)
166
- modifier_update('$pushAll', args)
167
- end
168
-
169
- def push_uniq(*args)
170
- criteria, keys = criteria_and_keys_from_args(args)
171
- keys.each { |key, value | criteria[key] = {'$ne' => value} }
172
- collection.update(criteria, {'$push' => keys}, :multi => true)
173
- end
174
-
175
- def pull(*args)
176
- modifier_update('$pull', args)
177
- end
178
-
179
- def pull_all(*args)
180
- modifier_update('$pullAll', args)
181
- end
182
-
183
- def pop(*args)
184
- modifier_update('$pop', args)
185
- end
186
-
187
149
  def embeddable?
188
150
  false
189
151
  end
@@ -200,7 +162,7 @@ module MongoMapper
200
162
  def set_database_name(name)
201
163
  @database_name = name
202
164
  end
203
-
165
+
204
166
  def database_name
205
167
  @database_name
206
168
  end
@@ -225,19 +187,6 @@ module MongoMapper
225
187
  database.collection(collection_name)
226
188
  end
227
189
 
228
- def timestamps!
229
- key :created_at, Time
230
- key :updated_at, Time
231
- class_eval { before_save :update_timestamps }
232
- end
233
-
234
- def userstamps!
235
- key :creator_id, ObjectId
236
- key :updater_id, ObjectId
237
- belongs_to :creator, :class_name => 'User'
238
- belongs_to :updater, :class_name => 'User'
239
- end
240
-
241
190
  def single_collection_inherited?
242
191
  keys.key?(:_type) && single_collection_inherited_superclass?
243
192
  end
@@ -258,18 +207,6 @@ module MongoMapper
258
207
  instances.size == 1 ? instances[0] : instances
259
208
  end
260
209
 
261
- def modifier_update(modifier, args)
262
- criteria, keys = criteria_and_keys_from_args(args)
263
- modifiers = {modifier => keys}
264
- collection.update(criteria, modifiers, :multi => true)
265
- end
266
-
267
- def criteria_and_keys_from_args(args)
268
- keys = args.pop
269
- criteria = args[0].is_a?(Hash) ? args[0] : {:id => args}
270
- [to_criteria(criteria), keys]
271
- end
272
-
273
210
  def assert_no_first_last_or_all(args)
274
211
  if args[0] == :first || args[0] == :last || args[0] == :all
275
212
  raise ArgumentError, "#{self}.find(:#{args}) is no longer supported, use #{self}.#{args} instead."
@@ -374,9 +311,14 @@ module MongoMapper
374
311
  end
375
312
 
376
313
  def delete
314
+ @_destroyed = true
377
315
  self.class.delete(id) unless new?
378
316
  end
379
317
 
318
+ def destroyed?
319
+ @_destroyed == true
320
+ end
321
+
380
322
  def reload
381
323
  if attrs = collection.find_one({:_id => _id})
382
324
  self.class.associations.each { |name, assoc| send(name).reset if respond_to?(name) }
@@ -387,6 +329,12 @@ module MongoMapper
387
329
  end
388
330
  end
389
331
 
332
+ # Used by embedded docs to find root easily without if/respond_to? stuff.
333
+ # Documents are always root documents.
334
+ def _root_document
335
+ self
336
+ end
337
+
390
338
  private
391
339
  def create_or_update(options={})
392
340
  result = new? ? create(options) : update(options)
@@ -406,12 +354,6 @@ module MongoMapper
406
354
  @new = false
407
355
  collection.save(to_mongo, :safe => safe)
408
356
  end
409
-
410
- def update_timestamps
411
- now = Time.now.utc
412
- self[:created_at] = now if new? && !created_at?
413
- self[:updated_at] = now
414
- end
415
357
  end
416
358
  end # Document
417
359
  end # MongoMapper
@@ -19,8 +19,9 @@ module MongoMapper
19
19
  plugin Plugins::Rails
20
20
  plugin Plugins::Serialization
21
21
  plugin Plugins::Validations
22
+ plugin Plugins::Callbacks
22
23
 
23
- attr_accessor :_root_document, :_parent_document
24
+ attr_reader :_root_document, :_parent_document
24
25
  end
25
26
 
26
27
  super
@@ -50,6 +51,11 @@ module MongoMapper
50
51
  end
51
52
  result
52
53
  end
54
+
55
+ def _parent_document=(value)
56
+ @_root_document = value._root_document
57
+ @_parent_document = value
58
+ end
53
59
  end # InstanceMethods
54
60
  end # EmbeddedDocument
55
61
  end # MongoMapper
@@ -20,9 +20,12 @@ module MongoMapper
20
20
  autoload :Inspect, 'mongo_mapper/plugins/inspect'
21
21
  autoload :Keys, 'mongo_mapper/plugins/keys'
22
22
  autoload :Logger, 'mongo_mapper/plugins/logger'
23
+ autoload :Modifiers, 'mongo_mapper/plugins/modifiers'
23
24
  autoload :Protected, 'mongo_mapper/plugins/protected'
24
25
  autoload :Rails, 'mongo_mapper/plugins/rails'
25
26
  autoload :Serialization, 'mongo_mapper/plugins/serialization'
27
+ autoload :Timestamps, 'mongo_mapper/plugins/timestamps'
28
+ autoload :Userstamps, 'mongo_mapper/plugins/userstamps'
26
29
  autoload :Validations, 'mongo_mapper/plugins/validations'
27
30
  end
28
31
  end
@@ -30,19 +30,8 @@ module MongoMapper
30
30
  alias_method :concat, :<<
31
31
 
32
32
  private
33
- def _root_document
34
- if owner.respond_to?(:_root_document)
35
- owner._root_document
36
- else
37
- owner
38
- end
39
- end
40
-
41
33
  def assign_references(*docs)
42
- docs.each do |doc|
43
- doc._root_document = _root_document
44
- doc._parent_document = owner
45
- end
34
+ docs.each { |doc| doc._parent_document = owner }
46
35
  end
47
36
  end
48
37
  end
@@ -79,6 +79,7 @@ module MongoMapper
79
79
  unless doc.new?
80
80
  ids << doc.id
81
81
  owner.save
82
+ reset
82
83
  end
83
84
  doc
84
85
  end
@@ -88,6 +89,7 @@ module MongoMapper
88
89
  unless doc.new?
89
90
  ids << doc.id
90
91
  owner.save
92
+ reset
91
93
  end
92
94
  doc
93
95
  end
@@ -123,7 +125,10 @@ module MongoMapper
123
125
  end
124
126
 
125
127
  def scoped_ids(args)
126
- args.flatten.reject { |id| !ids.include?(id) }
128
+ args.flatten.select do |id|
129
+ id = ObjectId.to_mongo(id) if klass.using_object_id?
130
+ ids.include?(id)
131
+ end
127
132
  end
128
133
 
129
134
  def find_target
@@ -52,18 +52,21 @@ module MongoMapper
52
52
  def build(attrs={})
53
53
  doc = klass.new(attrs)
54
54
  apply_scope(doc)
55
+ reset
55
56
  doc
56
57
  end
57
58
 
58
59
  def create(attrs={})
59
60
  doc = klass.new(attrs)
60
61
  apply_scope(doc).save
62
+ reset
61
63
  doc
62
64
  end
63
65
 
64
66
  def create!(attrs={})
65
67
  doc = klass.new(attrs)
66
68
  apply_scope(doc).save!
69
+ reset
67
70
  doc
68
71
  end
69
72
 
@@ -109,7 +112,7 @@ module MongoMapper
109
112
  end
110
113
 
111
114
  def foreign_key
112
- options[:foreign_key] || owner.class.name.underscore.gsub("/", "_") + "_id"
115
+ options[:foreign_key] || owner.class.name.to_s.underscore.gsub("/", "_") + "_id"
113
116
  end
114
117
  end
115
118
  end
@@ -1,35 +1,74 @@
1
+ # Almost all of this callback stuff is pulled directly from ActiveSupport
2
+ # in the interest of support rails 2 and 3 at the same time and is the
3
+ # same copyright as rails.
1
4
  module MongoMapper
2
5
  module Plugins
3
6
  module Callbacks
4
7
  def self.configure(model)
5
8
  model.class_eval do
6
- include ActiveSupport::Callbacks
7
-
8
9
  define_callbacks(
9
- :before_save, :after_save,
10
- :before_create, :after_create,
11
- :before_update, :after_update,
12
- :before_validation, :after_validation,
13
- :before_validation_on_create, :after_validation_on_create,
14
- :before_validation_on_update, :after_validation_on_update,
15
- :before_destroy, :after_destroy
10
+ :before_save, :after_save,
11
+ :before_create, :after_create,
12
+ :before_update, :after_update,
13
+ :before_validation, :after_validation,
14
+ :before_validation_on_create, :after_validation_on_create,
15
+ :before_validation_on_update, :after_validation_on_update,
16
+ :before_destroy, :after_destroy,
17
+ :validate_on_create, :validate_on_update,
18
+ :validate
16
19
  )
17
20
  end
18
21
  end
19
-
22
+
23
+ module ClassMethods
24
+ def define_callbacks(*callbacks)
25
+ callbacks.each do |callback|
26
+ class_eval <<-"end_eval"
27
+ def self.#{callback}(*methods, &block)
28
+ callbacks = CallbackChain.build(:#{callback}, *methods, &block)
29
+ @#{callback}_callbacks ||= CallbackChain.new
30
+ @#{callback}_callbacks.concat callbacks
31
+ end
32
+
33
+ def self.#{callback}_callback_chain
34
+ @#{callback}_callbacks ||= CallbackChain.new
35
+
36
+ if superclass.respond_to?(:#{callback}_callback_chain)
37
+ CallbackChain.new(
38
+ superclass.#{callback}_callback_chain +
39
+ @#{callback}_callbacks
40
+ )
41
+ else
42
+ @#{callback}_callbacks
43
+ end
44
+ end
45
+ end_eval
46
+ end
47
+ end
48
+ end
49
+
20
50
  module InstanceMethods
21
51
  def valid?
22
52
  action = new? ? 'create' : 'update'
23
-
24
53
  run_callbacks(:before_validation)
25
54
  run_callbacks("before_validation_on_#{action}".to_sym)
26
55
  result = super
27
56
  run_callbacks("after_validation_on_#{action}".to_sym)
28
57
  run_callbacks(:after_validation)
29
-
30
58
  result
31
59
  end
32
60
 
61
+ # Overriding validatable's valid_for_group? to integrate validation callbacks
62
+ def valid_for_group?(group) #:nodoc:
63
+ errors.clear
64
+ run_before_validations
65
+ run_callbacks(:validate)
66
+ new? ? run_callbacks(:validate_on_create) : run_callbacks(:validate_on_update)
67
+ self.class.validate_children(self, group)
68
+ self.validate_group(group)
69
+ errors.empty?
70
+ end
71
+
33
72
  def destroy
34
73
  run_callbacks(:before_destroy)
35
74
  result = super
@@ -37,6 +76,17 @@ module MongoMapper
37
76
  result
38
77
  end
39
78
 
79
+ def run_callbacks(kind, options = {}, &block)
80
+ callback_chain_method = "#{kind}_callback_chain"
81
+ return unless self.class.respond_to?(callback_chain_method)
82
+ self.class.send(callback_chain_method).run(self, options, &block)
83
+ self.embedded_associations.each do |association|
84
+ self.send(association.name).each do |document|
85
+ document.run_callbacks(kind, options, &block)
86
+ end
87
+ end
88
+ end
89
+
40
90
  private
41
91
  def create_or_update(*args)
42
92
  run_callbacks(:before_save)
@@ -60,6 +110,127 @@ module MongoMapper
60
110
  result
61
111
  end
62
112
  end
113
+
114
+ class CallbackChain < Array
115
+ def self.build(kind, *methods, &block)
116
+ methods, options = extract_options(*methods, &block)
117
+ methods.map! { |method| Callback.new(kind, method, options) }
118
+ new(methods)
119
+ end
120
+
121
+ def run(object, options = {}, &terminator)
122
+ enumerator = options[:enumerator] || :each
123
+
124
+ unless block_given?
125
+ send(enumerator) { |callback| callback.call(object) }
126
+ else
127
+ send(enumerator) do |callback|
128
+ result = callback.call(object)
129
+ break result if terminator.call(result, object)
130
+ end
131
+ end
132
+ end
133
+
134
+ # TODO: Decompose into more Array like behavior
135
+ def replace_or_append!(chain)
136
+ if index = index(chain)
137
+ self[index] = chain
138
+ else
139
+ self << chain
140
+ end
141
+ self
142
+ end
143
+
144
+ def find(callback, &block)
145
+ select { |c| c == callback && (!block_given? || yield(c)) }.first
146
+ end
147
+
148
+ def delete(callback)
149
+ super(callback.is_a?(Callback) ? callback : find(callback))
150
+ end
151
+
152
+ private
153
+ def self.extract_options(*methods, &block)
154
+ methods.flatten!
155
+ options = methods.extract_options!
156
+ methods << block if block_given?
157
+ return methods, options
158
+ end
159
+
160
+ def extract_options(*methods, &block)
161
+ self.class.extract_options(*methods, &block)
162
+ end
163
+ end
164
+
165
+ class Callback
166
+ attr_reader :kind, :method, :identifier, :options
167
+
168
+ def initialize(kind, method, options = {})
169
+ @kind = kind
170
+ @method = method
171
+ @identifier = options[:identifier]
172
+ @options = options
173
+ end
174
+
175
+ def ==(other)
176
+ case other
177
+ when Callback
178
+ (self.identifier && self.identifier == other.identifier) || self.method == other.method
179
+ else
180
+ (self.identifier && self.identifier == other) || self.method == other
181
+ end
182
+ end
183
+
184
+ def eql?(other)
185
+ self == other
186
+ end
187
+
188
+ def dup
189
+ self.class.new(@kind, @method, @options.dup)
190
+ end
191
+
192
+ def hash
193
+ if @identifier
194
+ @identifier.hash
195
+ else
196
+ @method.hash
197
+ end
198
+ end
199
+
200
+ def call(*args, &block)
201
+ evaluate_method(method, *args, &block) if should_run_callback?(*args)
202
+ rescue LocalJumpError
203
+ raise ArgumentError,
204
+ "Cannot yield from a Proc type filter. The Proc must take two " +
205
+ "arguments and execute #call on the second argument."
206
+ end
207
+
208
+ private
209
+ def evaluate_method(method, *args, &block)
210
+ case method
211
+ when Symbol
212
+ object = args.shift
213
+ object.send(method, *args, &block)
214
+ when String
215
+ eval(method, args.first.instance_eval { binding })
216
+ when Proc, Method
217
+ method.call(*args, &block)
218
+ else
219
+ if method.respond_to?(kind)
220
+ method.send(kind, *args, &block)
221
+ else
222
+ raise ArgumentError,
223
+ "Callbacks must be a symbol denoting the method to call, a string to be evaluated, " +
224
+ "a block to be invoked, or an object responding to the callback method."
225
+ end
226
+ end
227
+ end
228
+
229
+ def should_run_callback?(*args)
230
+ [options[:if]].flatten.compact.all? { |a| evaluate_method(a, *args) } &&
231
+ ![options[:unless]].flatten.compact.any? { |a| evaluate_method(a, *args) }
232
+ end
233
+ end
63
234
  end
64
235
  end
65
236
  end