mongo_mapper 0.7.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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