ign-mongo_mapper 0.8.6.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 (147) hide show
  1. data/LICENSE +20 -0
  2. data/README.rdoc +33 -0
  3. data/UPGRADES +7 -0
  4. data/bin/mmconsole +60 -0
  5. data/examples/attr_accessible.rb +22 -0
  6. data/examples/attr_protected.rb +22 -0
  7. data/examples/cache_key.rb +24 -0
  8. data/examples/custom_types.rb +24 -0
  9. data/examples/identity_map.rb +33 -0
  10. data/examples/identity_map/automatic.rb +8 -0
  11. data/examples/keys.rb +40 -0
  12. data/examples/modifiers/set.rb +25 -0
  13. data/examples/plugins.rb +41 -0
  14. data/examples/querying.rb +35 -0
  15. data/examples/safe.rb +43 -0
  16. data/examples/scopes.rb +52 -0
  17. data/examples/validating/embedded_docs.rb +29 -0
  18. data/lib/mongo_mapper.rb +83 -0
  19. data/lib/mongo_mapper/connection.rb +83 -0
  20. data/lib/mongo_mapper/document.rb +41 -0
  21. data/lib/mongo_mapper/embedded_document.rb +31 -0
  22. data/lib/mongo_mapper/exceptions.rb +27 -0
  23. data/lib/mongo_mapper/extensions/array.rb +19 -0
  24. data/lib/mongo_mapper/extensions/binary.rb +22 -0
  25. data/lib/mongo_mapper/extensions/boolean.rb +44 -0
  26. data/lib/mongo_mapper/extensions/date.rb +25 -0
  27. data/lib/mongo_mapper/extensions/float.rb +14 -0
  28. data/lib/mongo_mapper/extensions/hash.rb +14 -0
  29. data/lib/mongo_mapper/extensions/integer.rb +19 -0
  30. data/lib/mongo_mapper/extensions/kernel.rb +9 -0
  31. data/lib/mongo_mapper/extensions/nil_class.rb +18 -0
  32. data/lib/mongo_mapper/extensions/object.rb +27 -0
  33. data/lib/mongo_mapper/extensions/object_id.rb +30 -0
  34. data/lib/mongo_mapper/extensions/set.rb +20 -0
  35. data/lib/mongo_mapper/extensions/string.rb +18 -0
  36. data/lib/mongo_mapper/extensions/time.rb +29 -0
  37. data/lib/mongo_mapper/middleware/identity_map.rb +16 -0
  38. data/lib/mongo_mapper/plugins.rb +15 -0
  39. data/lib/mongo_mapper/plugins/accessible.rb +44 -0
  40. data/lib/mongo_mapper/plugins/associations.rb +134 -0
  41. data/lib/mongo_mapper/plugins/associations/base.rb +124 -0
  42. data/lib/mongo_mapper/plugins/associations/belongs_to_polymorphic_proxy.rb +29 -0
  43. data/lib/mongo_mapper/plugins/associations/belongs_to_proxy.rb +24 -0
  44. data/lib/mongo_mapper/plugins/associations/collection.rb +27 -0
  45. data/lib/mongo_mapper/plugins/associations/embedded_collection.rb +40 -0
  46. data/lib/mongo_mapper/plugins/associations/in_array_proxy.rb +151 -0
  47. data/lib/mongo_mapper/plugins/associations/many_documents_as_proxy.rb +28 -0
  48. data/lib/mongo_mapper/plugins/associations/many_documents_proxy.rb +109 -0
  49. data/lib/mongo_mapper/plugins/associations/many_embedded_polymorphic_proxy.rb +32 -0
  50. data/lib/mongo_mapper/plugins/associations/many_embedded_proxy.rb +24 -0
  51. data/lib/mongo_mapper/plugins/associations/many_polymorphic_proxy.rb +14 -0
  52. data/lib/mongo_mapper/plugins/associations/one_embedded_proxy.rb +41 -0
  53. data/lib/mongo_mapper/plugins/associations/one_proxy.rb +68 -0
  54. data/lib/mongo_mapper/plugins/associations/proxy.rb +139 -0
  55. data/lib/mongo_mapper/plugins/caching.rb +21 -0
  56. data/lib/mongo_mapper/plugins/callbacks.rb +243 -0
  57. data/lib/mongo_mapper/plugins/clone.rb +22 -0
  58. data/lib/mongo_mapper/plugins/descendants.rb +17 -0
  59. data/lib/mongo_mapper/plugins/dirty.rb +124 -0
  60. data/lib/mongo_mapper/plugins/document.rb +41 -0
  61. data/lib/mongo_mapper/plugins/dynamic_querying.rb +43 -0
  62. data/lib/mongo_mapper/plugins/dynamic_querying/dynamic_finder.rb +44 -0
  63. data/lib/mongo_mapper/plugins/embedded_document.rb +48 -0
  64. data/lib/mongo_mapper/plugins/equality.rb +17 -0
  65. data/lib/mongo_mapper/plugins/identity_map.rb +128 -0
  66. data/lib/mongo_mapper/plugins/indexes.rb +12 -0
  67. data/lib/mongo_mapper/plugins/inspect.rb +15 -0
  68. data/lib/mongo_mapper/plugins/keys.rb +311 -0
  69. data/lib/mongo_mapper/plugins/keys/key.rb +65 -0
  70. data/lib/mongo_mapper/plugins/logger.rb +18 -0
  71. data/lib/mongo_mapper/plugins/modifiers.rb +112 -0
  72. data/lib/mongo_mapper/plugins/pagination.rb +14 -0
  73. data/lib/mongo_mapper/plugins/persistence.rb +69 -0
  74. data/lib/mongo_mapper/plugins/protected.rb +53 -0
  75. data/lib/mongo_mapper/plugins/querying.rb +176 -0
  76. data/lib/mongo_mapper/plugins/querying/decorator.rb +46 -0
  77. data/lib/mongo_mapper/plugins/querying/plucky_methods.rb +15 -0
  78. data/lib/mongo_mapper/plugins/rails.rb +58 -0
  79. data/lib/mongo_mapper/plugins/safe.rb +28 -0
  80. data/lib/mongo_mapper/plugins/sci.rb +32 -0
  81. data/lib/mongo_mapper/plugins/scopes.rb +21 -0
  82. data/lib/mongo_mapper/plugins/serialization.rb +76 -0
  83. data/lib/mongo_mapper/plugins/timestamps.rb +22 -0
  84. data/lib/mongo_mapper/plugins/userstamps.rb +15 -0
  85. data/lib/mongo_mapper/plugins/validations.rb +50 -0
  86. data/lib/mongo_mapper/support/descendant_appends.rb +45 -0
  87. data/lib/mongo_mapper/version.rb +4 -0
  88. data/rails/init.rb +15 -0
  89. data/test/_NOTE_ON_TESTING +1 -0
  90. data/test/functional/associations/test_belongs_to_polymorphic_proxy.rb +64 -0
  91. data/test/functional/associations/test_belongs_to_proxy.rb +117 -0
  92. data/test/functional/associations/test_in_array_proxy.rb +349 -0
  93. data/test/functional/associations/test_many_documents_as_proxy.rb +229 -0
  94. data/test/functional/associations/test_many_documents_proxy.rb +615 -0
  95. data/test/functional/associations/test_many_embedded_polymorphic_proxy.rb +176 -0
  96. data/test/functional/associations/test_many_embedded_proxy.rb +256 -0
  97. data/test/functional/associations/test_many_polymorphic_proxy.rb +303 -0
  98. data/test/functional/associations/test_one_embedded_proxy.rb +100 -0
  99. data/test/functional/associations/test_one_proxy.rb +206 -0
  100. data/test/functional/test_accessible.rb +168 -0
  101. data/test/functional/test_associations.rb +46 -0
  102. data/test/functional/test_binary.rb +27 -0
  103. data/test/functional/test_caching.rb +76 -0
  104. data/test/functional/test_callbacks.rb +151 -0
  105. data/test/functional/test_dirty.rb +163 -0
  106. data/test/functional/test_document.rb +272 -0
  107. data/test/functional/test_dynamic_querying.rb +75 -0
  108. data/test/functional/test_embedded_document.rb +210 -0
  109. data/test/functional/test_identity_map.rb +513 -0
  110. data/test/functional/test_indexes.rb +42 -0
  111. data/test/functional/test_logger.rb +20 -0
  112. data/test/functional/test_modifiers.rb +416 -0
  113. data/test/functional/test_pagination.rb +91 -0
  114. data/test/functional/test_protected.rb +175 -0
  115. data/test/functional/test_querying.rb +873 -0
  116. data/test/functional/test_safe.rb +76 -0
  117. data/test/functional/test_sci.rb +230 -0
  118. data/test/functional/test_scopes.rb +171 -0
  119. data/test/functional/test_string_id_compatibility.rb +67 -0
  120. data/test/functional/test_timestamps.rb +62 -0
  121. data/test/functional/test_userstamps.rb +27 -0
  122. data/test/functional/test_validations.rb +342 -0
  123. data/test/models.rb +233 -0
  124. data/test/test_active_model_lint.rb +13 -0
  125. data/test/test_helper.rb +102 -0
  126. data/test/unit/associations/test_base.rb +212 -0
  127. data/test/unit/associations/test_proxy.rb +105 -0
  128. data/test/unit/serializers/test_json_serializer.rb +217 -0
  129. data/test/unit/test_clone.rb +69 -0
  130. data/test/unit/test_descendant_appends.rb +71 -0
  131. data/test/unit/test_document.rb +208 -0
  132. data/test/unit/test_dynamic_finder.rb +125 -0
  133. data/test/unit/test_embedded_document.rb +639 -0
  134. data/test/unit/test_extensions.rb +376 -0
  135. data/test/unit/test_identity_map_middleware.rb +34 -0
  136. data/test/unit/test_inspect.rb +22 -0
  137. data/test/unit/test_key.rb +205 -0
  138. data/test/unit/test_keys.rb +89 -0
  139. data/test/unit/test_mongo_mapper.rb +110 -0
  140. data/test/unit/test_pagination.rb +11 -0
  141. data/test/unit/test_plugins.rb +50 -0
  142. data/test/unit/test_rails.rb +181 -0
  143. data/test/unit/test_rails_compatibility.rb +52 -0
  144. data/test/unit/test_serialization.rb +51 -0
  145. data/test/unit/test_time_zones.rb +39 -0
  146. data/test/unit/test_validations.rb +564 -0
  147. metadata +385 -0
@@ -0,0 +1,32 @@
1
+ # encoding: UTF-8
2
+ module MongoMapper
3
+ module Plugins
4
+ module Associations
5
+ class ManyEmbeddedPolymorphicProxy < EmbeddedCollection
6
+ def replace(values)
7
+ @_values = values.map do |v|
8
+ v.respond_to?(:attributes) ? v.attributes.merge(association.type_key_name => v.class.name) : v
9
+ end
10
+ reset
11
+ end
12
+
13
+ protected
14
+ def find_target
15
+ (@_values || []).map do |hash|
16
+ child = polymorphic_class(hash).load(hash)
17
+ assign_references(child)
18
+ child
19
+ end
20
+ end
21
+
22
+ def polymorphic_class(doc)
23
+ if class_name = doc[association.type_key_name]
24
+ class_name.constantize
25
+ else
26
+ klass
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,24 @@
1
+ # encoding: UTF-8
2
+ module MongoMapper
3
+ module Plugins
4
+ module Associations
5
+ class ManyEmbeddedProxy < EmbeddedCollection
6
+ def replace(values)
7
+ @_values = values.map do |v|
8
+ v.respond_to?(:attributes) ? v.attributes : v
9
+ end
10
+ reset
11
+ end
12
+
13
+ private
14
+ def find_target
15
+ (@_values || []).map do |attrs|
16
+ klass.load(attrs).tap do |child|
17
+ assign_references(child)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,14 @@
1
+ # encoding: UTF-8
2
+ module MongoMapper
3
+ module Plugins
4
+ module Associations
5
+ class ManyPolymorphicProxy < ManyDocumentsProxy
6
+ private
7
+ def apply_scope(doc)
8
+ doc[association.type_key_name] = doc.class.name
9
+ super
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,41 @@
1
+ # encoding: UTF-8
2
+ module MongoMapper
3
+ module Plugins
4
+ module Associations
5
+ class OneEmbeddedProxy < Proxy
6
+ def build(attributes={})
7
+ @target = klass.new(attributes)
8
+ assign_references(@target)
9
+ loaded
10
+ @target
11
+ end
12
+
13
+ def replace(doc)
14
+ if doc.respond_to?(:attributes)
15
+ @target = klass.load(doc.attributes)
16
+ else
17
+ @target = klass.load(doc)
18
+ end
19
+ @target.default_id_value if @target && @target.id.nil?
20
+ assign_references(@target)
21
+ loaded
22
+ @target
23
+ end
24
+
25
+ protected
26
+
27
+ def find_target
28
+ if @value
29
+ klass.load(@value).tap do |child|
30
+ assign_references(child)
31
+ end
32
+ end
33
+ end
34
+
35
+ def assign_references(doc)
36
+ doc._parent_document = proxy_owner if doc
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,68 @@
1
+ # encoding: UTF-8
2
+ module MongoMapper
3
+ module Plugins
4
+ module Associations
5
+ class OneProxy < Proxy
6
+ def build(attrs={})
7
+ instantiate_target(:new, attrs)
8
+ end
9
+
10
+ def create(attrs={})
11
+ instantiate_target(:create, attrs)
12
+ end
13
+
14
+ def create!(attrs={})
15
+ instantiate_target(:create!, attrs)
16
+ end
17
+
18
+ def replace(doc)
19
+ load_target
20
+
21
+ if !target.nil? && target != doc
22
+ if options[:dependent] && !target.new?
23
+ case options[:dependent]
24
+ when :delete
25
+ target.delete
26
+ when :destroy
27
+ target.destroy
28
+ when :nullify
29
+ target[foreign_key] = nil
30
+ target.save
31
+ end
32
+ end
33
+ end
34
+
35
+ if doc.nil?
36
+ target.update_attributes(foreign_key => nil) unless target.nil?
37
+ else
38
+ proxy_owner.save if proxy_owner.new?
39
+ doc = klass.new(doc) unless doc.is_a?(klass)
40
+ doc[foreign_key] = proxy_owner.id
41
+ doc.save if doc.new?
42
+ loaded
43
+ @target = doc
44
+ end
45
+ end
46
+
47
+ protected
48
+ def find_target
49
+ target_class.first(association.query_options.merge(foreign_key => proxy_owner.id))
50
+ end
51
+
52
+ def instantiate_target(instantiator, attrs={})
53
+ @target = target_class.send(instantiator, attrs.update(foreign_key => proxy_owner.id))
54
+ loaded
55
+ @target
56
+ end
57
+
58
+ def target_class
59
+ @target_class ||= options[:class] || (options[:class_name] || association.name.to_s.camelize).constantize
60
+ end
61
+
62
+ def foreign_key
63
+ options[:foreign_key] || proxy_owner.class.name.foreign_key
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,139 @@
1
+ # encoding: UTF-8
2
+ require 'forwardable'
3
+ module MongoMapper
4
+ module Plugins
5
+ module Associations
6
+ class Proxy
7
+ extend Forwardable
8
+
9
+ alias :proxy_respond_to? :respond_to?
10
+ alias :proxy_extend :extend
11
+
12
+ instance_methods.each { |m| undef_method m unless m =~ /(^__|^nil\?$|^send$|proxy_|^object_id$)/ }
13
+
14
+ attr_reader :proxy_owner, :association, :target
15
+
16
+ alias :proxy_target :target
17
+ alias :proxy_association :association
18
+
19
+ def_delegators :proxy_association, :klass, :options
20
+ def_delegator :klass, :collection
21
+
22
+ def initialize(owner, association)
23
+ @proxy_owner, @association, @loaded = owner, association, false
24
+ Array(association.options[:extend]).each { |ext| proxy_extend(ext) }
25
+ reset
26
+ end
27
+
28
+ # Active support in rails 3 beta 4 can override to_json after this is loaded,
29
+ # at least when run in mongomapper tests. The implementation was changed in master
30
+ # some time after this, so not sure whether this is still a problem.
31
+ #
32
+ # In rails 2, this isn't a problem however it also solves an issue where
33
+ # to_json isn't forwarded because it supports to_json itself
34
+ def to_json(*options)
35
+ load_target
36
+ target.to_json(*options)
37
+ end
38
+
39
+ # see comments to to_json
40
+ def as_json(*options)
41
+ load_target
42
+ target.as_json(*options)
43
+ end
44
+
45
+ def inspect
46
+ load_target
47
+ target.inspect
48
+ end
49
+
50
+ def loaded?
51
+ @loaded
52
+ end
53
+
54
+ def loaded
55
+ @loaded = true
56
+ end
57
+
58
+ def nil?
59
+ load_target
60
+ target.nil?
61
+ end
62
+
63
+ def blank?
64
+ load_target
65
+ target.blank?
66
+ end
67
+
68
+ def present?
69
+ load_target
70
+ target.present?
71
+ end
72
+
73
+ def reload
74
+ reset
75
+ load_target
76
+ self unless target.nil?
77
+ end
78
+
79
+ def replace(v)
80
+ raise NotImplementedError
81
+ end
82
+
83
+ def reset
84
+ @loaded = false
85
+ @target = nil
86
+ end
87
+
88
+ def respond_to?(*args)
89
+ proxy_respond_to?(*args) || (load_target && target.respond_to?(*args))
90
+ end
91
+
92
+ def send(method, *args)
93
+ if proxy_respond_to?(method)
94
+ super
95
+ else
96
+ load_target
97
+ target.send(method, *args)
98
+ end
99
+ end
100
+
101
+ def ===(other)
102
+ load_target
103
+ other === target
104
+ end
105
+
106
+ protected
107
+ def method_missing(method, *args, &block)
108
+ if load_target
109
+ target.send(method, *args, &block)
110
+ end
111
+ end
112
+
113
+ def load_target
114
+ unless loaded?
115
+ if @target.is_a?(Array) && @target.any?
116
+ @target = find_target + @target.find_all { |record| record.new? }
117
+ else
118
+ @target = find_target
119
+ end
120
+ loaded
121
+ end
122
+ @target
123
+ rescue MongoMapper::DocumentNotFound
124
+ reset
125
+ end
126
+
127
+ def find_target
128
+ raise NotImplementedError
129
+ end
130
+
131
+ def flatten_deeper(array)
132
+ array.collect do |element|
133
+ (element.respond_to?(:flatten) && !element.is_a?(Hash)) ? element.flatten : element
134
+ end.flatten
135
+ end
136
+ end
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,21 @@
1
+ # encoding: UTF-8
2
+ module MongoMapper
3
+ module Plugins
4
+ module Caching
5
+ module InstanceMethods
6
+ def cache_key(*suffixes)
7
+ cache_key = case
8
+ when new?
9
+ "#{self.class.name}/new"
10
+ when timestamp = self[:updated_at]
11
+ "#{self.class.name}/#{id}-#{timestamp.to_s(:number)}"
12
+ else
13
+ "#{self.class.name}/#{id}"
14
+ end
15
+ cache_key += "/#{suffixes.join('/')}" unless suffixes.empty?
16
+ cache_key
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,243 @@
1
+ # encoding: UTF-8
2
+ # Almost all of this callback stuff is pulled directly from ActiveSupport
3
+ # in the interest of support rails 2 and 3 at the same time and is the
4
+ # same copyright as rails.
5
+ module MongoMapper
6
+ module Plugins
7
+ module Callbacks
8
+ def self.configure(model)
9
+ model.class_eval do
10
+ define_callbacks(
11
+ :before_save, :after_save,
12
+ :before_create, :after_create,
13
+ :before_update, :after_update,
14
+ :before_validation, :after_validation,
15
+ :before_validation_on_create, :after_validation_on_create,
16
+ :before_validation_on_update, :after_validation_on_update,
17
+ :before_destroy, :after_destroy,
18
+ :validate_on_create, :validate_on_update,
19
+ :validate
20
+ )
21
+ end
22
+ end
23
+
24
+ module ClassMethods
25
+ def define_callbacks(*callbacks)
26
+ callbacks.each do |callback|
27
+ class_eval <<-"end_eval"
28
+ def self.#{callback}(*methods, &block)
29
+ callbacks = CallbackChain.build(:#{callback}, *methods, &block)
30
+ @#{callback}_callbacks ||= CallbackChain.new
31
+ @#{callback}_callbacks.concat callbacks
32
+ end
33
+
34
+ def self.#{callback}_callback_chain
35
+ @#{callback}_callbacks ||= CallbackChain.new
36
+
37
+ if superclass.respond_to?(:#{callback}_callback_chain)
38
+ CallbackChain.new(
39
+ superclass.#{callback}_callback_chain +
40
+ @#{callback}_callbacks
41
+ )
42
+ else
43
+ @#{callback}_callbacks
44
+ end
45
+ end
46
+ end_eval
47
+ end
48
+ end
49
+ end
50
+
51
+ module InstanceMethods
52
+ def valid?
53
+ action = new? ? 'create' : 'update'
54
+ run_callbacks(:before_validation)
55
+ run_callbacks("before_validation_on_#{action}".to_sym)
56
+ result = super
57
+ run_callbacks("after_validation_on_#{action}".to_sym)
58
+ run_callbacks(:after_validation)
59
+ result
60
+ end
61
+
62
+ # Overriding validatable's valid_for_group? to integrate validation callbacks
63
+ def valid_for_group?(group) #:nodoc:
64
+ errors.clear
65
+ run_before_validations
66
+ run_callbacks(:validate)
67
+ new? ? run_callbacks(:validate_on_create) : run_callbacks(:validate_on_update)
68
+ self.class.validate_children(self, group)
69
+ self.validate_group(group)
70
+ errors.empty?
71
+ end
72
+
73
+ def destroy
74
+ run_callbacks(:before_destroy)
75
+ result = super
76
+ run_callbacks(:after_destroy)
77
+ result
78
+ end
79
+
80
+ def run_callbacks(kind, options={}, &block)
81
+ callback_chain_method = "#{kind}_callback_chain"
82
+ return unless self.class.respond_to?(callback_chain_method)
83
+ self.class.send(callback_chain_method).run(self, options, &block)
84
+ self.embedded_associations.each do |association|
85
+ if association.one?
86
+ if !self.send("#{association.name}").nil?
87
+ self.send(association.name).run_callbacks(kind, options, &block)
88
+ end
89
+ else
90
+ self.send(association.name).each do |document|
91
+ document.run_callbacks(kind, options, &block)
92
+ end
93
+ end
94
+ end
95
+ end
96
+
97
+ private
98
+ def create_or_update(*args)
99
+ run_callbacks(:before_save)
100
+ if result = super
101
+ run_callbacks(:after_save)
102
+ end
103
+ result
104
+ end
105
+
106
+ def create(*args)
107
+ run_callbacks(:before_create)
108
+ result = super
109
+ run_callbacks(:after_create)
110
+ result
111
+ end
112
+
113
+ def update(*args)
114
+ run_callbacks(:before_update)
115
+ result = super
116
+ run_callbacks(:after_update)
117
+ result
118
+ end
119
+ end
120
+
121
+ class CallbackChain < Array
122
+ def self.build(kind, *methods, &block)
123
+ methods, options = extract_options(*methods, &block)
124
+ methods.map! { |method| Callback.new(kind, method, options) }
125
+ new(methods)
126
+ end
127
+
128
+ def run(object, options={}, &terminator)
129
+ enumerator = options[:enumerator] || :each
130
+
131
+ unless block_given?
132
+ send(enumerator) { |callback| callback.call(object) }
133
+ else
134
+ send(enumerator) do |callback|
135
+ result = callback.call(object)
136
+ break result if terminator.call(result, object)
137
+ end
138
+ end
139
+ end
140
+
141
+ # TODO: Decompose into more Array like behavior
142
+ def replace_or_append!(chain)
143
+ if index = index(chain)
144
+ self[index] = chain
145
+ else
146
+ self << chain
147
+ end
148
+ self
149
+ end
150
+
151
+ def find(callback, &block)
152
+ select { |c| c == callback && (!block_given? || yield(c)) }.first
153
+ end
154
+
155
+ def delete(callback)
156
+ super(callback.is_a?(Callback) ? callback : find(callback))
157
+ end
158
+
159
+ private
160
+ def self.extract_options(*methods, &block)
161
+ methods.flatten!
162
+ options = methods.extract_options!
163
+ methods << block if block_given?
164
+ return methods, options
165
+ end
166
+
167
+ def extract_options(*methods, &block)
168
+ self.class.extract_options(*methods, &block)
169
+ end
170
+ end
171
+
172
+ class Callback
173
+ attr_reader :kind, :method, :identifier, :options
174
+
175
+ def initialize(kind, method, options={})
176
+ @kind = kind
177
+ @method = method
178
+ @identifier = options[:identifier]
179
+ @options = options
180
+ end
181
+
182
+ def ==(other)
183
+ case other
184
+ when Callback
185
+ (self.identifier && self.identifier == other.identifier) || self.method == other.method
186
+ else
187
+ (self.identifier && self.identifier == other) || self.method == other
188
+ end
189
+ end
190
+
191
+ def eql?(other)
192
+ self == other
193
+ end
194
+
195
+ def dup
196
+ self.class.new(@kind, @method, @options.dup)
197
+ end
198
+
199
+ def hash
200
+ if @identifier
201
+ @identifier.hash
202
+ else
203
+ @method.hash
204
+ end
205
+ end
206
+
207
+ def call(*args, &block)
208
+ evaluate_method(method, *args, &block) if should_run_callback?(*args)
209
+ rescue LocalJumpError
210
+ raise ArgumentError,
211
+ "Cannot yield from a Proc type filter. The Proc must take two " +
212
+ "arguments and execute #call on the second argument."
213
+ end
214
+
215
+ private
216
+ def evaluate_method(method, *args, &block)
217
+ case method
218
+ when Symbol
219
+ object = args.shift
220
+ object.send(method, *args, &block)
221
+ when String
222
+ eval(method, args.first.instance_eval { binding })
223
+ when Proc, Method
224
+ method.call(*args, &block)
225
+ else
226
+ if method.respond_to?(kind)
227
+ method.send(kind, *args, &block)
228
+ else
229
+ raise ArgumentError,
230
+ "Callbacks must be a symbol denoting the method to call, a string to be evaluated, " +
231
+ "a block to be invoked, or an object responding to the callback method."
232
+ end
233
+ end
234
+ end
235
+
236
+ def should_run_callback?(*args)
237
+ [options[:if]].flatten.compact.all? { |a| evaluate_method(a, *args) } &&
238
+ ![options[:unless]].flatten.compact.any? { |a| evaluate_method(a, *args) }
239
+ end
240
+ end
241
+ end
242
+ end
243
+ end