dm-core 0.10.2 → 1.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (183) hide show
  1. data/.gitignore +10 -1
  2. data/Gemfile +143 -0
  3. data/Rakefile +9 -5
  4. data/VERSION +1 -1
  5. data/dm-core.gemspec +160 -57
  6. data/lib/dm-core.rb +131 -56
  7. data/lib/dm-core/adapters.rb +98 -14
  8. data/lib/dm-core/adapters/abstract_adapter.rb +24 -4
  9. data/lib/dm-core/adapters/in_memory_adapter.rb +7 -2
  10. data/lib/dm-core/associations/many_to_many.rb +19 -30
  11. data/lib/dm-core/associations/many_to_one.rb +58 -42
  12. data/lib/dm-core/associations/one_to_many.rb +33 -23
  13. data/lib/dm-core/associations/one_to_one.rb +27 -11
  14. data/lib/dm-core/associations/relationship.rb +4 -4
  15. data/lib/dm-core/collection.rb +23 -16
  16. data/lib/dm-core/core_ext/array.rb +36 -0
  17. data/lib/dm-core/core_ext/hash.rb +30 -0
  18. data/lib/dm-core/core_ext/module.rb +46 -0
  19. data/lib/dm-core/core_ext/object.rb +31 -0
  20. data/lib/dm-core/core_ext/pathname.rb +20 -0
  21. data/lib/dm-core/core_ext/string.rb +22 -0
  22. data/lib/dm-core/core_ext/try_dup.rb +44 -0
  23. data/lib/dm-core/model.rb +88 -27
  24. data/lib/dm-core/model/hook.rb +75 -18
  25. data/lib/dm-core/model/property.rb +50 -9
  26. data/lib/dm-core/model/relationship.rb +31 -31
  27. data/lib/dm-core/model/scope.rb +3 -3
  28. data/lib/dm-core/property.rb +196 -516
  29. data/lib/dm-core/property/binary.rb +7 -0
  30. data/lib/dm-core/property/boolean.rb +35 -0
  31. data/lib/dm-core/property/class.rb +24 -0
  32. data/lib/dm-core/property/date.rb +47 -0
  33. data/lib/dm-core/property/date_time.rb +48 -0
  34. data/lib/dm-core/property/decimal.rb +43 -0
  35. data/lib/dm-core/property/discriminator.rb +48 -0
  36. data/lib/dm-core/property/float.rb +24 -0
  37. data/lib/dm-core/property/integer.rb +32 -0
  38. data/lib/dm-core/property/numeric.rb +43 -0
  39. data/lib/dm-core/property/object.rb +32 -0
  40. data/lib/dm-core/property/serial.rb +8 -0
  41. data/lib/dm-core/property/string.rb +49 -0
  42. data/lib/dm-core/property/text.rb +12 -0
  43. data/lib/dm-core/property/time.rb +48 -0
  44. data/lib/dm-core/property/typecast/numeric.rb +32 -0
  45. data/lib/dm-core/property/typecast/time.rb +28 -0
  46. data/lib/dm-core/property_set.rb +10 -4
  47. data/lib/dm-core/query.rb +14 -37
  48. data/lib/dm-core/query/conditions/comparison.rb +8 -6
  49. data/lib/dm-core/query/conditions/operation.rb +33 -2
  50. data/lib/dm-core/query/operator.rb +2 -5
  51. data/lib/dm-core/query/path.rb +4 -6
  52. data/lib/dm-core/repository.rb +21 -6
  53. data/lib/dm-core/resource.rb +316 -133
  54. data/lib/dm-core/resource/state.rb +79 -0
  55. data/lib/dm-core/resource/state/clean.rb +40 -0
  56. data/lib/dm-core/resource/state/deleted.rb +30 -0
  57. data/lib/dm-core/resource/state/dirty.rb +86 -0
  58. data/lib/dm-core/resource/state/immutable.rb +34 -0
  59. data/lib/dm-core/resource/state/persisted.rb +29 -0
  60. data/lib/dm-core/resource/state/transient.rb +70 -0
  61. data/lib/dm-core/spec/lib/adapter_helpers.rb +52 -0
  62. data/lib/dm-core/spec/lib/collection_helpers.rb +20 -0
  63. data/{spec → lib/dm-core/spec}/lib/counter_adapter.rb +5 -1
  64. data/lib/dm-core/spec/lib/pending_helpers.rb +50 -0
  65. data/lib/dm-core/spec/lib/spec_helper.rb +68 -0
  66. data/lib/dm-core/spec/setup.rb +165 -0
  67. data/lib/dm-core/spec/{adapter_shared_spec.rb → shared/adapter_spec.rb} +21 -7
  68. data/{spec/public/shared/resource_shared_spec.rb → lib/dm-core/spec/shared/resource_spec.rb} +120 -83
  69. data/{spec/public/shared/sel_shared_spec.rb → lib/dm-core/spec/shared/sel_spec.rb} +5 -6
  70. data/lib/dm-core/support/assertions.rb +8 -0
  71. data/lib/dm-core/support/equalizer.rb +1 -0
  72. data/lib/dm-core/support/hook.rb +420 -0
  73. data/lib/dm-core/support/lazy_array.rb +453 -0
  74. data/lib/dm-core/support/local_object_space.rb +12 -0
  75. data/lib/dm-core/support/logger.rb +193 -6
  76. data/lib/dm-core/support/naming_conventions.rb +8 -8
  77. data/lib/dm-core/support/subject.rb +33 -0
  78. data/lib/dm-core/type.rb +4 -0
  79. data/lib/dm-core/types/boolean.rb +2 -0
  80. data/lib/dm-core/types/decimal.rb +9 -0
  81. data/lib/dm-core/types/discriminator.rb +2 -0
  82. data/lib/dm-core/types/object.rb +3 -0
  83. data/lib/dm-core/types/serial.rb +2 -0
  84. data/lib/dm-core/types/text.rb +2 -0
  85. data/lib/dm-core/version.rb +1 -1
  86. data/spec/public/associations/many_to_many/read_multiple_join_spec.rb +67 -0
  87. data/spec/public/model/hook_spec.rb +209 -0
  88. data/spec/public/model/property_spec.rb +35 -0
  89. data/spec/public/model/relationship_spec.rb +33 -20
  90. data/spec/public/model_spec.rb +142 -10
  91. data/spec/public/property/binary_spec.rb +14 -0
  92. data/spec/public/property/boolean_spec.rb +14 -0
  93. data/spec/public/property/class_spec.rb +20 -0
  94. data/spec/public/property/date_spec.rb +14 -0
  95. data/spec/public/property/date_time_spec.rb +14 -0
  96. data/spec/public/property/decimal_spec.rb +14 -0
  97. data/spec/public/{types → property}/discriminator_spec.rb +2 -12
  98. data/spec/public/property/float_spec.rb +14 -0
  99. data/spec/public/property/integer_spec.rb +14 -0
  100. data/spec/public/property/object_spec.rb +9 -17
  101. data/spec/public/property/serial_spec.rb +14 -0
  102. data/spec/public/property/string_spec.rb +14 -0
  103. data/spec/public/property/text_spec.rb +52 -0
  104. data/spec/public/property/time_spec.rb +14 -0
  105. data/spec/public/property_spec.rb +28 -87
  106. data/spec/public/resource_spec.rb +101 -0
  107. data/spec/public/sel_spec.rb +5 -15
  108. data/spec/public/shared/collection_shared_spec.rb +16 -30
  109. data/spec/public/shared/finder_shared_spec.rb +2 -4
  110. data/spec/public/shared/property_shared_spec.rb +176 -0
  111. data/spec/semipublic/adapters/abstract_adapter_spec.rb +1 -1
  112. data/spec/semipublic/adapters/in_memory_adapter_spec.rb +2 -2
  113. data/spec/semipublic/associations/many_to_many_spec.rb +89 -0
  114. data/spec/semipublic/associations/many_to_one_spec.rb +24 -1
  115. data/spec/semipublic/associations/one_to_many_spec.rb +51 -0
  116. data/spec/semipublic/associations/one_to_one_spec.rb +49 -0
  117. data/spec/semipublic/associations/relationship_spec.rb +3 -3
  118. data/spec/semipublic/associations_spec.rb +1 -1
  119. data/spec/semipublic/property/binary_spec.rb +13 -0
  120. data/spec/semipublic/property/boolean_spec.rb +65 -0
  121. data/spec/semipublic/property/class_spec.rb +33 -0
  122. data/spec/semipublic/property/date_spec.rb +43 -0
  123. data/spec/semipublic/property/date_time_spec.rb +46 -0
  124. data/spec/semipublic/property/decimal_spec.rb +82 -0
  125. data/spec/semipublic/property/discriminator_spec.rb +19 -0
  126. data/spec/semipublic/property/float_spec.rb +82 -0
  127. data/spec/semipublic/property/integer_spec.rb +82 -0
  128. data/spec/semipublic/property/serial_spec.rb +13 -0
  129. data/spec/semipublic/property/string_spec.rb +13 -0
  130. data/spec/semipublic/property/text_spec.rb +31 -0
  131. data/spec/semipublic/property/time_spec.rb +50 -0
  132. data/spec/semipublic/property_spec.rb +2 -532
  133. data/spec/semipublic/query/conditions/comparison_spec.rb +171 -169
  134. data/spec/semipublic/query/conditions/operation_spec.rb +53 -51
  135. data/spec/semipublic/query/path_spec.rb +17 -17
  136. data/spec/semipublic/query_spec.rb +47 -78
  137. data/spec/semipublic/resource/state/clean_spec.rb +88 -0
  138. data/spec/semipublic/resource/state/deleted_spec.rb +78 -0
  139. data/spec/semipublic/resource/state/dirty_spec.rb +133 -0
  140. data/spec/semipublic/resource/state/immutable_spec.rb +99 -0
  141. data/spec/semipublic/resource/state/transient_spec.rb +128 -0
  142. data/spec/semipublic/resource/state_spec.rb +226 -0
  143. data/spec/semipublic/shared/property_shared_spec.rb +143 -0
  144. data/spec/semipublic/shared/resource_shared_spec.rb +16 -15
  145. data/spec/semipublic/shared/resource_state_shared_spec.rb +78 -0
  146. data/spec/semipublic/shared/subject_shared_spec.rb +79 -0
  147. data/spec/spec_helper.rb +21 -97
  148. data/spec/support/types/huge_integer.rb +17 -0
  149. data/spec/unit/array_spec.rb +48 -0
  150. data/spec/unit/hash_spec.rb +35 -0
  151. data/spec/unit/hook_spec.rb +1234 -0
  152. data/spec/unit/lazy_array_spec.rb +1959 -0
  153. data/spec/unit/module_spec.rb +70 -0
  154. data/spec/unit/object_spec.rb +37 -0
  155. data/spec/unit/try_dup_spec.rb +45 -0
  156. data/tasks/local_gemfile.rake +18 -0
  157. data/tasks/spec.rake +0 -3
  158. metadata +197 -71
  159. data/deps.rip +0 -2
  160. data/lib/dm-core/adapters/data_objects_adapter.rb +0 -712
  161. data/lib/dm-core/adapters/mysql_adapter.rb +0 -42
  162. data/lib/dm-core/adapters/oracle_adapter.rb +0 -229
  163. data/lib/dm-core/adapters/postgres_adapter.rb +0 -22
  164. data/lib/dm-core/adapters/sqlite3_adapter.rb +0 -17
  165. data/lib/dm-core/adapters/sqlserver_adapter.rb +0 -114
  166. data/lib/dm-core/adapters/yaml_adapter.rb +0 -111
  167. data/lib/dm-core/core_ext/enumerable.rb +0 -28
  168. data/lib/dm-core/migrations.rb +0 -1427
  169. data/lib/dm-core/spec/data_objects_adapter_shared_spec.rb +0 -366
  170. data/lib/dm-core/transaction.rb +0 -508
  171. data/lib/dm-core/types/paranoid_boolean.rb +0 -42
  172. data/lib/dm-core/types/paranoid_datetime.rb +0 -41
  173. data/spec/lib/adapter_helpers.rb +0 -105
  174. data/spec/lib/collection_helpers.rb +0 -18
  175. data/spec/lib/pending_helpers.rb +0 -46
  176. data/spec/public/migrations_spec.rb +0 -503
  177. data/spec/public/transaction_spec.rb +0 -153
  178. data/spec/semipublic/adapters/mysql_adapter_spec.rb +0 -17
  179. data/spec/semipublic/adapters/oracle_adapter_spec.rb +0 -194
  180. data/spec/semipublic/adapters/postgres_adapter_spec.rb +0 -17
  181. data/spec/semipublic/adapters/sqlite3_adapter_spec.rb +0 -17
  182. data/spec/semipublic/adapters/sqlserver_adapter_spec.rb +0 -17
  183. data/spec/semipublic/adapters/yaml_adapter_spec.rb +0 -12
@@ -20,7 +20,7 @@ share_examples_for 'A Collection supporting Strategic Eager Loading' do
20
20
  before :all do
21
21
  @original_adapter = @adapter
22
22
 
23
- @adapter.meta_class.class_eval do
23
+ @adapter.singleton_class.class_eval do
24
24
  def eql?(other)
25
25
  super || self == other
26
26
  end
@@ -28,6 +28,7 @@ share_examples_for 'A Collection supporting Strategic Eager Loading' do
28
28
 
29
29
  @adapter = DataMapper::Repository.adapters[@adapter.name] = CounterAdapter.new(@adapter)
30
30
  @repository.instance_variable_set(:@adapter, @adapter)
31
+ @articles.instance_variable_get(:@query).instance_variable_set(:@repository, @repository)
31
32
  end
32
33
 
33
34
  before :all do
@@ -75,7 +76,7 @@ share_examples_for 'A Resource supporting Strategic Eager Loading' do
75
76
  before :all do
76
77
  @original_adapter = @adapter
77
78
 
78
- @adapter.meta_class.class_eval do
79
+ @adapter.singleton_class.class_eval do
79
80
  def eql?(other)
80
81
  super || other == self
81
82
  end
@@ -86,10 +87,8 @@ share_examples_for 'A Resource supporting Strategic Eager Loading' do
86
87
  end
87
88
 
88
89
  before :all do
89
- @results = []
90
-
91
- @user_model.all.each do |user|
92
- @results << [ user, user.referrer ]
90
+ @results = @user_model.all.map do |user|
91
+ [ user, user.referrer ]
93
92
  end
94
93
 
95
94
  # some storage engines return the data in a different order
@@ -0,0 +1,8 @@
1
+ module DataMapper
2
+ module Assertions
3
+ def assert_kind_of(name, value, *klasses)
4
+ klasses.each { |k| return if value.kind_of?(k) }
5
+ raise ArgumentError, "+#{name}+ should be #{klasses.map { |k| k.name } * ' or '}, but was #{value.class.name}", caller(2)
6
+ end
7
+ end
8
+ end
@@ -30,6 +30,7 @@ module DataMapper
30
30
  class_eval <<-RUBY, __FILE__, __LINE__ + 1
31
31
  def ==(other)
32
32
  return true if equal?(other)
33
+ return false unless kind_of?(other.class) || other.kind_of?(self.class)
33
34
  #{respond_to.join(' && ')} &&
34
35
  #{equivalent.join(' && ')}
35
36
  end
@@ -0,0 +1,420 @@
1
+ require 'dm-core/support/assertions'
2
+ require 'dm-core/support/local_object_space'
3
+
4
+ # Mainly done here to support spec/unit/hook_spec without
5
+ # needing to require dm-core to setup either extlib or AS
6
+
7
+ begin
8
+ require 'active_support/core_ext/object/singleton_class'
9
+ rescue LoadError
10
+ class Object
11
+ unless respond_to?(:singleton_class)
12
+ def singleton_class
13
+ class << self; self end
14
+ end
15
+ end
16
+ end
17
+ end
18
+
19
+ module DataMapper
20
+ #
21
+ # TODO: Write more documentation!
22
+ #
23
+ # Overview
24
+ # ========
25
+ #
26
+ # The Hook module is a very simple set of AOP helpers. Basically, it
27
+ # allows the developer to specify a method or block that should run
28
+ # before or after another method.
29
+ #
30
+ # Usage
31
+ # =====
32
+ #
33
+ # Halting The Hook Stack
34
+ #
35
+ # Inheritance
36
+ #
37
+ # Other Goodies
38
+ #
39
+ # Please bring up any issues regarding Hooks with carllerche on IRC
40
+ #
41
+ module Hook
42
+
43
+ def self.included(base)
44
+ base.extend(ClassMethods)
45
+ base.const_set("CLASS_HOOKS", {}) unless base.const_defined?("CLASS_HOOKS")
46
+ base.const_set("INSTANCE_HOOKS", {}) unless base.const_defined?("INSTANCE_HOOKS")
47
+ base.class_eval do
48
+ class << self
49
+ def method_added(name)
50
+ process_method_added(name, :instance)
51
+ end
52
+
53
+ def singleton_method_added(name)
54
+ process_method_added(name, :class)
55
+ end
56
+ end
57
+ end
58
+ end
59
+
60
+ module ClassMethods
61
+ extend DataMapper::LocalObjectSpace
62
+ include DataMapper::Assertions
63
+ # Inject code that executes before the target class method.
64
+ #
65
+ # @param target_method<Symbol> the name of the class method to inject before
66
+ # @param method_sym<Symbol> the name of the method to run before the
67
+ # target_method
68
+ # @param block<Block> the code to run before the target_method
69
+ #
70
+ # @note
71
+ # Either method_sym or block is required.
72
+ # -
73
+ # @api public
74
+ def before_class_method(target_method, method_sym = nil, &block)
75
+ install_hook :before, target_method, method_sym, :class, &block
76
+ end
77
+
78
+ #
79
+ # Inject code that executes after the target class method.
80
+ #
81
+ # @param target_method<Symbol> the name of the class method to inject after
82
+ # @param method_sym<Symbol> the name of the method to run after the target_method
83
+ # @param block<Block> the code to run after the target_method
84
+ #
85
+ # @note
86
+ # Either method_sym or block is required.
87
+ # -
88
+ # @api public
89
+ def after_class_method(target_method, method_sym = nil, &block)
90
+ install_hook :after, target_method, method_sym, :class, &block
91
+ end
92
+
93
+ #
94
+ # Inject code that executes before the target instance method.
95
+ #
96
+ # @param target_method<Symbol> the name of the instance method to inject before
97
+ # @param method_sym<Symbol> the name of the method to run before the
98
+ # target_method
99
+ # @param block<Block> the code to run before the target_method
100
+ #
101
+ # @note
102
+ # Either method_sym or block is required.
103
+ # -
104
+ # @api public
105
+ def before(target_method, method_sym = nil, &block)
106
+ install_hook :before, target_method, method_sym, :instance, &block
107
+ end
108
+
109
+ #
110
+ # Inject code that executes after the target instance method.
111
+ #
112
+ # @param target_method<Symbol> the name of the instance method to inject after
113
+ # @param method_sym<Symbol> the name of the method to run after the
114
+ # target_method
115
+ # @param block<Block> the code to run after the target_method
116
+ #
117
+ # @note
118
+ # Either method_sym or block is required.
119
+ # -
120
+ # @api public
121
+ def after(target_method, method_sym = nil, &block)
122
+ install_hook :after, target_method, method_sym, :instance, &block
123
+ end
124
+
125
+ # Register a class method as hookable. Registering a method means that
126
+ # before hooks will be run immediately before the method is invoked and
127
+ # after hooks will be called immediately after the method is invoked.
128
+ #
129
+ # @param hookable_method<Symbol> The name of the class method that should
130
+ # be hookable
131
+ # -
132
+ # @api public
133
+ def register_class_hooks(*hooks)
134
+ hooks.each { |hook| register_hook(hook, :class) }
135
+ end
136
+
137
+ # Register aninstance method as hookable. Registering a method means that
138
+ # before hooks will be run immediately before the method is invoked and
139
+ # after hooks will be called immediately after the method is invoked.
140
+ #
141
+ # @param hookable_method<Symbol> The name of the instance method that should
142
+ # be hookable
143
+ # -
144
+ # @api public
145
+ def register_instance_hooks(*hooks)
146
+ hooks.each { |hook| register_hook(hook, :instance) }
147
+ end
148
+
149
+ # Not yet implemented
150
+ def reset_hook!(target_method, scope)
151
+ raise NotImplementedError
152
+ end
153
+
154
+ # --- Alright kids... the rest is internal stuff ---
155
+
156
+ # Returns the correct HOOKS Hash depending on whether we are
157
+ # working with class methods or instance methods
158
+ def hooks_with_scope(scope)
159
+ case scope
160
+ when :class then class_hooks
161
+ when :instance then instance_hooks
162
+ else raise ArgumentError, 'You need to pass :class or :instance as scope'
163
+ end
164
+ end
165
+
166
+ def class_hooks
167
+ self.const_get("CLASS_HOOKS")
168
+ end
169
+
170
+ def instance_hooks
171
+ self.const_get("INSTANCE_HOOKS")
172
+ end
173
+
174
+ # Registers a method as hookable. Registering hooks involves the following
175
+ # process
176
+ #
177
+ # * Create a blank entry in the HOOK Hash for the method.
178
+ # * Define the methods that execute the before and after hook stack.
179
+ # These methods will be no-ops at first, but everytime a new hook is
180
+ # defined, the methods will be redefined to incorporate the new hook.
181
+ # * Redefine the method that is to be hookable so that the hook stacks
182
+ # are invoked approprietly.
183
+ def register_hook(target_method, scope)
184
+ if scope == :instance && !method_defined?(target_method)
185
+ raise ArgumentError, "#{target_method} instance method does not exist"
186
+ elsif scope == :class && !respond_to?(target_method)
187
+ raise ArgumentError, "#{target_method} class method does not exist"
188
+ end
189
+
190
+ hooks = hooks_with_scope(scope)
191
+
192
+ if hooks[target_method].nil?
193
+ hooks[target_method] = {
194
+ # We need to keep track of which class in the Inheritance chain the
195
+ # method was declared hookable in. Every time a child declares a new
196
+ # hook for the method, the hook stack invocations need to be redefined
197
+ # in the original Class. See #define_hook_stack_execution_methods
198
+ :before => [], :after => [], :in => self
199
+ }
200
+
201
+ define_hook_stack_execution_methods(target_method, scope)
202
+ define_advised_method(target_method, scope)
203
+ end
204
+ end
205
+
206
+ # Is the method registered as a hookable in the given scope.
207
+ def registered_as_hook?(target_method, scope)
208
+ ! hooks_with_scope(scope)[target_method].nil?
209
+ end
210
+
211
+ # Generates names for the various utility methods. We need to do this because
212
+ # the various utility methods should not end in = so, while we're at it, we
213
+ # might as well get rid of all punctuation.
214
+ def hook_method_name(target_method, prefix, suffix)
215
+ target_method = target_method.to_s
216
+
217
+ case target_method[-1,1]
218
+ when '?' then "#{prefix}_#{target_method[0..-2]}_ques_#{suffix}"
219
+ when '!' then "#{prefix}_#{target_method[0..-2]}_bang_#{suffix}"
220
+ when '=' then "#{prefix}_#{target_method[0..-2]}_eq_#{suffix}"
221
+ # I add a _nan_ suffix here so that we don't ever encounter
222
+ # any naming conflicts.
223
+ else "#{prefix}_#{target_method[0..-1]}_nan_#{suffix}"
224
+ end
225
+ end
226
+
227
+ # This will need to be refactored
228
+ def process_method_added(method_name, scope)
229
+ hooks_with_scope(scope).each do |target_method, hooks|
230
+ if hooks[:before].any? { |hook| hook[:name] == method_name }
231
+ define_hook_stack_execution_methods(target_method, scope)
232
+ end
233
+
234
+ if hooks[:after].any? { |hook| hook[:name] == method_name }
235
+ define_hook_stack_execution_methods(target_method, scope)
236
+ end
237
+ end
238
+ end
239
+
240
+ # Defines two methods. One method executes the before hook stack. The other executes
241
+ # the after hook stack. This method will be called many times during the Class definition
242
+ # process. It should be called for each hook that is defined. It will also be called
243
+ # when a hook is redefined (to make sure that the arity hasn't changed).
244
+ def define_hook_stack_execution_methods(target_method, scope)
245
+ unless registered_as_hook?(target_method, scope)
246
+ raise ArgumentError, "#{target_method} has not be registered as a hookable #{scope} method"
247
+ end
248
+
249
+ hooks = hooks_with_scope(scope)
250
+
251
+ before_hooks = hooks[target_method][:before]
252
+ before_hooks = before_hooks.map{ |info| inline_call(info, scope) }.join("\n")
253
+
254
+ after_hooks = hooks[target_method][:after]
255
+ after_hooks = after_hooks.map{ |info| inline_call(info, scope) }.join("\n")
256
+
257
+ before_hook_name = hook_method_name(target_method, 'execute_before', 'hook_stack')
258
+ after_hook_name = hook_method_name(target_method, 'execute_after', 'hook_stack')
259
+
260
+ hooks[target_method][:in].class_eval <<-RUBY, __FILE__, __LINE__ + 1
261
+ #{scope == :class ? 'class << self' : ''}
262
+
263
+ private
264
+
265
+ remove_method :#{before_hook_name} if instance_methods(false).any? { |m| m.to_sym == :#{before_hook_name} }
266
+ def #{before_hook_name}(*args)
267
+ #{before_hooks}
268
+ end
269
+
270
+ remove_method :#{after_hook_name} if instance_methods(false).any? { |m| m.to_sym == :#{after_hook_name} }
271
+ def #{after_hook_name}(*args)
272
+ #{after_hooks}
273
+ end
274
+
275
+ #{scope == :class ? 'end' : ''}
276
+ RUBY
277
+ end
278
+
279
+ # Returns ruby code that will invoke the hook. It checks the arity of the hook method
280
+ # and passes arguments accordingly.
281
+ def inline_call(method_info, scope)
282
+ DataMapper::Hook::ClassMethods.hook_scopes << method_info[:from]
283
+ name = method_info[:name]
284
+ if scope == :instance
285
+ args = method_defined?(name) && instance_method(name).arity != 0 ? '*args' : ''
286
+ %(#{name}(#{args}) if self.class <= DataMapper::Hook::ClassMethods.object_by_id(#{method_info[:from].object_id}))
287
+ else
288
+ args = respond_to?(name) && method(name).arity != 0 ? '*args' : ''
289
+ %(#{name}(#{args}) if self <= DataMapper::Hook::ClassMethods.object_by_id(#{method_info[:from].object_id}))
290
+ end
291
+ end
292
+
293
+ def define_advised_method(target_method, scope)
294
+ args = args_for(method_with_scope(target_method, scope))
295
+
296
+ renamed_target = hook_method_name(target_method, 'hookable_', 'before_advised')
297
+
298
+ source = <<-EOD
299
+ def #{target_method}(#{args})
300
+ retval = nil
301
+ catch(:halt) do
302
+ #{hook_method_name(target_method, 'execute_before', 'hook_stack')}(#{args})
303
+ retval = #{renamed_target}(#{args})
304
+ #{hook_method_name(target_method, 'execute_after', 'hook_stack')}(retval, #{args})
305
+ retval
306
+ end
307
+ end
308
+ EOD
309
+
310
+ if scope == :instance && !instance_methods(false).any? { |m| m.to_sym == target_method }
311
+ send(:alias_method, renamed_target, target_method)
312
+
313
+ proxy_module = Module.new
314
+ proxy_module.class_eval(source, __FILE__, __LINE__)
315
+ self.send(:include, proxy_module)
316
+ else
317
+ source = %{alias_method :#{renamed_target}, :#{target_method}\n#{source}}
318
+ source = %{class << self\n#{source}\nend} if scope == :class
319
+ class_eval(source, __FILE__, __LINE__)
320
+ end
321
+ end
322
+
323
+ # --- Add a hook ---
324
+
325
+ def install_hook(type, target_method, method_sym, scope, &block)
326
+ assert_kind_of 'target_method', target_method, Symbol
327
+ assert_kind_of 'method_sym', method_sym, Symbol unless method_sym.nil?
328
+ assert_kind_of 'scope', scope, Symbol
329
+
330
+ if !block_given? and method_sym.nil?
331
+ raise ArgumentError, "You need to pass 2 arguments to \"#{type}\"."
332
+ end
333
+
334
+ if method_sym.to_s[-1,1] == '='
335
+ raise ArgumentError, "Methods ending in = cannot be hooks"
336
+ end
337
+
338
+ unless [ :class, :instance ].include?(scope)
339
+ raise ArgumentError, 'You need to pass :class or :instance as scope'
340
+ end
341
+
342
+ if registered_as_hook?(target_method, scope)
343
+ hooks = hooks_with_scope(scope)
344
+
345
+ #if this hook is previously declared in a sibling or cousin we must move the :in class
346
+ #to the common ancestor to get both hooks to run.
347
+ if !(hooks[target_method][:in] <=> self)
348
+ before_hook_name = hook_method_name(target_method, 'execute_before', 'hook_stack')
349
+ after_hook_name = hook_method_name(target_method, 'execute_after', 'hook_stack')
350
+
351
+ hooks[target_method][:in].class_eval <<-RUBY, __FILE__, __LINE__ + 1
352
+ remove_method :#{before_hook_name} if instance_methods(false).any? { |m| m.to_sym == :#{before_hook_name} }
353
+ def #{before_hook_name}(*args)
354
+ super
355
+ end
356
+
357
+ remove_method :#{after_hook_name} if instance_methods(false).any? { |m| m.to_sym == :#{before_hook_name} }
358
+ def #{after_hook_name}(*args)
359
+ super
360
+ end
361
+ RUBY
362
+
363
+ while !(hooks[target_method][:in] <=> self) do
364
+ hooks[target_method][:in] = hooks[target_method][:in].superclass
365
+ end
366
+
367
+ define_hook_stack_execution_methods(target_method, scope)
368
+ hooks[target_method][:in].class_eval{define_advised_method(target_method, scope)}
369
+ end
370
+ else
371
+ register_hook(target_method, scope)
372
+ hooks = hooks_with_scope(scope)
373
+ end
374
+
375
+ #if we were passed a block, create a method out of it.
376
+ if block
377
+ method_sym = "__hooks_#{type}_#{quote_method(target_method)}_#{hooks[target_method][type].length}".to_sym
378
+ if scope == :class
379
+ singleton_class.instance_eval do
380
+ define_method(method_sym, &block)
381
+ end
382
+ else
383
+ define_method(method_sym, &block)
384
+ end
385
+ end
386
+
387
+ # Adds method to the stack an redefines the hook invocation method
388
+ hooks[target_method][type] << { :name => method_sym, :from => self }
389
+ define_hook_stack_execution_methods(target_method, scope)
390
+ end
391
+
392
+ # --- Helpers ---
393
+
394
+ def args_for(method)
395
+ if method.arity == 0
396
+ "&block"
397
+ elsif method.arity > 0
398
+ "_" << (1 .. method.arity).to_a.join(", _") << ", &block"
399
+ elsif (method.arity + 1) < 0
400
+ "_" << (1 .. (method.arity).abs - 1).to_a.join(", _") << ", *args, &block"
401
+ else
402
+ "*args, &block"
403
+ end
404
+ end
405
+
406
+ def method_with_scope(name, scope)
407
+ case scope
408
+ when :class then method(name)
409
+ when :instance then instance_method(name)
410
+ else raise ArgumentError, 'You need to pass :class or :instance as scope'
411
+ end
412
+ end
413
+
414
+ def quote_method(name)
415
+ name.to_s.gsub(/\?$/, '_q_').gsub(/!$/, '_b_').gsub(/=$/, '_eq_')
416
+ end
417
+ end
418
+
419
+ end
420
+ end