ardm 0.0.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 (179) hide show
  1. checksums.yaml +15 -0
  2. data/.gitignore +35 -0
  3. data/Gemfile +13 -0
  4. data/LICENSE +21 -0
  5. data/README.md +70 -0
  6. data/Rakefile +4 -0
  7. data/ardm.gemspec +29 -0
  8. data/db/.gitignore +1 -0
  9. data/lib/ardm/active_record/associations.rb +80 -0
  10. data/lib/ardm/active_record/base.rb +49 -0
  11. data/lib/ardm/active_record/dirty.rb +25 -0
  12. data/lib/ardm/active_record/hooks.rb +31 -0
  13. data/lib/ardm/active_record/inheritance.rb +37 -0
  14. data/lib/ardm/active_record/is/state_machine.rb +21 -0
  15. data/lib/ardm/active_record/is.rb +22 -0
  16. data/lib/ardm/active_record/not_found.rb +7 -0
  17. data/lib/ardm/active_record/predicate_builder/array_handler.rb +31 -0
  18. data/lib/ardm/active_record/predicate_builder/rails3.rb +147 -0
  19. data/lib/ardm/active_record/predicate_builder/rails4.rb +139 -0
  20. data/lib/ardm/active_record/predicate_builder/relation_handler.rb +15 -0
  21. data/lib/ardm/active_record/predicate_builder.rb +19 -0
  22. data/lib/ardm/active_record/property.rb +357 -0
  23. data/lib/ardm/active_record/query.rb +108 -0
  24. data/lib/ardm/active_record/record.rb +70 -0
  25. data/lib/ardm/active_record/relation.rb +83 -0
  26. data/lib/ardm/active_record/repository.rb +38 -0
  27. data/lib/ardm/active_record/serialization.rb +164 -0
  28. data/lib/ardm/active_record/storage_names.rb +28 -0
  29. data/lib/ardm/active_record/validations.rb +111 -0
  30. data/lib/ardm/active_record.rb +43 -0
  31. data/lib/ardm/data_mapper/not_found.rb +5 -0
  32. data/lib/ardm/data_mapper/record.rb +41 -0
  33. data/lib/ardm/data_mapper.rb +5 -0
  34. data/lib/ardm/env.rb +5 -0
  35. data/lib/ardm/property/api_key.rb +30 -0
  36. data/lib/ardm/property/bcrypt_hash.rb +31 -0
  37. data/lib/ardm/property/binary.rb +23 -0
  38. data/lib/ardm/property/boolean.rb +29 -0
  39. data/lib/ardm/property/class.rb +19 -0
  40. data/lib/ardm/property/comma_separated_list.rb +28 -0
  41. data/lib/ardm/property/csv.rb +35 -0
  42. data/lib/ardm/property/date.rb +12 -0
  43. data/lib/ardm/property/datetime.rb +12 -0
  44. data/lib/ardm/property/decimal.rb +38 -0
  45. data/lib/ardm/property/discriminator.rb +65 -0
  46. data/lib/ardm/property/enum.rb +51 -0
  47. data/lib/ardm/property/epoch_time.rb +38 -0
  48. data/lib/ardm/property/file_path.rb +25 -0
  49. data/lib/ardm/property/flag.rb +65 -0
  50. data/lib/ardm/property/float.rb +18 -0
  51. data/lib/ardm/property/integer.rb +24 -0
  52. data/lib/ardm/property/invalid_value_error.rb +22 -0
  53. data/lib/ardm/property/ip_address.rb +35 -0
  54. data/lib/ardm/property/json.rb +49 -0
  55. data/lib/ardm/property/lookup.rb +29 -0
  56. data/lib/ardm/property/numeric.rb +40 -0
  57. data/lib/ardm/property/object.rb +36 -0
  58. data/lib/ardm/property/paranoid_boolean.rb +18 -0
  59. data/lib/ardm/property/paranoid_datetime.rb +17 -0
  60. data/lib/ardm/property/regexp.rb +22 -0
  61. data/lib/ardm/property/serial.rb +16 -0
  62. data/lib/ardm/property/slug.rb +29 -0
  63. data/lib/ardm/property/string.rb +40 -0
  64. data/lib/ardm/property/support/dirty_minder.rb +169 -0
  65. data/lib/ardm/property/support/flags.rb +41 -0
  66. data/lib/ardm/property/support/paranoid_base.rb +78 -0
  67. data/lib/ardm/property/text.rb +11 -0
  68. data/lib/ardm/property/time.rb +12 -0
  69. data/lib/ardm/property/uri.rb +27 -0
  70. data/lib/ardm/property/uuid.rb +65 -0
  71. data/lib/ardm/property/validation.rb +208 -0
  72. data/lib/ardm/property/yaml.rb +38 -0
  73. data/lib/ardm/property.rb +891 -0
  74. data/lib/ardm/property_set.rb +152 -0
  75. data/lib/ardm/query/expression.rb +85 -0
  76. data/lib/ardm/query/ext/symbol.rb +37 -0
  77. data/lib/ardm/query/operator.rb +64 -0
  78. data/lib/ardm/record.rb +1 -0
  79. data/lib/ardm/support/assertions.rb +8 -0
  80. data/lib/ardm/support/deprecate.rb +12 -0
  81. data/lib/ardm/support/descendant_set.rb +89 -0
  82. data/lib/ardm/support/equalizer.rb +48 -0
  83. data/lib/ardm/support/ext/array.rb +22 -0
  84. data/lib/ardm/support/ext/blank.rb +25 -0
  85. data/lib/ardm/support/ext/hash.rb +67 -0
  86. data/lib/ardm/support/ext/module.rb +47 -0
  87. data/lib/ardm/support/ext/object.rb +57 -0
  88. data/lib/ardm/support/ext/string.rb +24 -0
  89. data/lib/ardm/support/ext/try_dup.rb +12 -0
  90. data/lib/ardm/support/hook.rb +405 -0
  91. data/lib/ardm/support/lazy_array.rb +451 -0
  92. data/lib/ardm/support/local_object_space.rb +13 -0
  93. data/lib/ardm/support/logger.rb +201 -0
  94. data/lib/ardm/support/mash.rb +176 -0
  95. data/lib/ardm/support/naming_conventions.rb +90 -0
  96. data/lib/ardm/support/ordered_set.rb +380 -0
  97. data/lib/ardm/support/subject.rb +33 -0
  98. data/lib/ardm/support/subject_set.rb +250 -0
  99. data/lib/ardm/version.rb +3 -0
  100. data/lib/ardm.rb +56 -0
  101. data/spec/fixtures/api_user.rb +11 -0
  102. data/spec/fixtures/article.rb +22 -0
  103. data/spec/fixtures/bookmark.rb +14 -0
  104. data/spec/fixtures/invention.rb +5 -0
  105. data/spec/fixtures/network_node.rb +23 -0
  106. data/spec/fixtures/person.rb +17 -0
  107. data/spec/fixtures/software_package.rb +22 -0
  108. data/spec/fixtures/ticket.rb +12 -0
  109. data/spec/fixtures/tshirt.rb +15 -0
  110. data/spec/integration/api_key_spec.rb +25 -0
  111. data/spec/integration/bcrypt_hash_spec.rb +45 -0
  112. data/spec/integration/comma_separated_list_spec.rb +85 -0
  113. data/spec/integration/dirty_minder_spec.rb +197 -0
  114. data/spec/integration/enum_spec.rb +79 -0
  115. data/spec/integration/epoch_time_spec.rb +59 -0
  116. data/spec/integration/file_path_spec.rb +158 -0
  117. data/spec/integration/flag_spec.rb +72 -0
  118. data/spec/integration/ip_address_spec.rb +151 -0
  119. data/spec/integration/json_spec.rb +70 -0
  120. data/spec/integration/slug_spec.rb +65 -0
  121. data/spec/integration/uri_spec.rb +136 -0
  122. data/spec/integration/uuid_spec.rb +102 -0
  123. data/spec/integration/yaml_spec.rb +85 -0
  124. data/spec/public/property/binary_spec.rb +41 -0
  125. data/spec/public/property/boolean_spec.rb +30 -0
  126. data/spec/public/property/class_spec.rb +28 -0
  127. data/spec/public/property/date_spec.rb +22 -0
  128. data/spec/public/property/date_time_spec.rb +22 -0
  129. data/spec/public/property/decimal_spec.rb +23 -0
  130. data/spec/public/property/discriminator_spec.rb +133 -0
  131. data/spec/public/property/float_spec.rb +22 -0
  132. data/spec/public/property/integer_spec.rb +22 -0
  133. data/spec/public/property/object_spec.rb +103 -0
  134. data/spec/public/property/serial_spec.rb +22 -0
  135. data/spec/public/property/string_spec.rb +22 -0
  136. data/spec/public/property/text_spec.rb +23 -0
  137. data/spec/public/property/time_spec.rb +22 -0
  138. data/spec/public/property_spec.rb +316 -0
  139. data/spec/rcov.opts +6 -0
  140. data/spec/schema.rb +86 -0
  141. data/spec/semipublic/property/binary_spec.rb +14 -0
  142. data/spec/semipublic/property/boolean_spec.rb +48 -0
  143. data/spec/semipublic/property/class_spec.rb +36 -0
  144. data/spec/semipublic/property/date_spec.rb +44 -0
  145. data/spec/semipublic/property/date_time_spec.rb +47 -0
  146. data/spec/semipublic/property/decimal_spec.rb +83 -0
  147. data/spec/semipublic/property/discriminator_spec.rb +22 -0
  148. data/spec/semipublic/property/float_spec.rb +83 -0
  149. data/spec/semipublic/property/integer_spec.rb +83 -0
  150. data/spec/semipublic/property/lookup_spec.rb +27 -0
  151. data/spec/semipublic/property/serial_spec.rb +14 -0
  152. data/spec/semipublic/property/string_spec.rb +14 -0
  153. data/spec/semipublic/property/text_spec.rb +30 -0
  154. data/spec/semipublic/property/time_spec.rb +49 -0
  155. data/spec/semipublic/property_spec.rb +51 -0
  156. data/spec/shared/flags_shared_spec.rb +36 -0
  157. data/spec/shared/identity_function_group.rb +5 -0
  158. data/spec/shared/public_property_spec.rb +229 -0
  159. data/spec/shared/semipublic_property_spec.rb +159 -0
  160. data/spec/spec.opts +4 -0
  161. data/spec/spec_helper.rb +58 -0
  162. data/spec/unit/bcrypt_hash_spec.rb +154 -0
  163. data/spec/unit/csv_spec.rb +139 -0
  164. data/spec/unit/dirty_minder_spec.rb +64 -0
  165. data/spec/unit/enum_spec.rb +125 -0
  166. data/spec/unit/epoch_time_spec.rb +72 -0
  167. data/spec/unit/file_path_spec.rb +75 -0
  168. data/spec/unit/flag_spec.rb +114 -0
  169. data/spec/unit/ip_address_spec.rb +109 -0
  170. data/spec/unit/json_spec.rb +127 -0
  171. data/spec/unit/paranoid_boolean_spec.rb +142 -0
  172. data/spec/unit/paranoid_datetime_spec.rb +149 -0
  173. data/spec/unit/regexp_spec.rb +62 -0
  174. data/spec/unit/uri_spec.rb +64 -0
  175. data/spec/unit/yaml_spec.rb +111 -0
  176. data/tasks/spec.rake +40 -0
  177. data/tasks/yard.rake +9 -0
  178. data/tasks/yardstick.rake +19 -0
  179. metadata +350 -0
@@ -0,0 +1,57 @@
1
+ module Ardm; module Ext
2
+ module Object
3
+ # Returns the value of the specified constant.
4
+ #
5
+ # @overload full_const_get(obj, name)
6
+ # Returns the value of the specified constant in +obj+.
7
+ # @param [Object] obj The root object used as origin.
8
+ # @param [String] name The name of the constant to get, e.g. "Merb::Router".
9
+ #
10
+ # @overload full_const_get(name)
11
+ # Returns the value of the fully-qualified constant.
12
+ # @param [String] name The name of the constant to get, e.g. "Merb::Router".
13
+ #
14
+ # @return [Object] The constant corresponding to +name+.
15
+ #
16
+ # @api semipublic
17
+ def self.full_const_get(obj, name = nil)
18
+ obj, name = ::Object, obj if name.nil?
19
+
20
+ list = name.split("::")
21
+ list.shift if Ardm::Ext.blank?(list.first)
22
+ list.each do |x|
23
+ # This is required because const_get tries to look for constants in the
24
+ # ancestor chain, but we only want constants that are HERE
25
+ obj = obj.const_defined?(x) ? obj.const_get(x) : obj.const_missing(x)
26
+ end
27
+ obj
28
+ end
29
+
30
+ # Sets the specified constant to the given +value+.
31
+ #
32
+ # @overload full_const_set(obj, name)
33
+ # Sets the specified constant in +obj+ to the given +value+.
34
+ # @param [Object] obj The root object used as origin.
35
+ # @param [String] name The name of the constant to set, e.g. "Merb::Router".
36
+ # @param [Object] value The value to assign to the constant.
37
+ #
38
+ # @overload full_const_set(name)
39
+ # Sets the fully-qualified constant to the given +value+.
40
+ # @param [String] name The name of the constant to set, e.g. "Merb::Router".
41
+ # @param [Object] value The value to assign to the constant.
42
+ #
43
+ # @return [Object] The constant corresponding to +name+.
44
+ #
45
+ # @api semipublic
46
+ def self.full_const_set(obj, name, value = nil)
47
+ obj, name, value = ::Object, obj, name if value.nil?
48
+
49
+ list = name.split("::")
50
+ toplevel = Ardm::Ext.blank?(list.first)
51
+ list.shift if toplevel
52
+ last = list.pop
53
+ obj = list.empty? ? ::Object : Ardm::Ext::Object.full_const_get(list.join("::"))
54
+ obj.const_set(last, value) if obj && !obj.const_defined?(last)
55
+ end
56
+ end
57
+ end; end
@@ -0,0 +1,24 @@
1
+ module Ardm; module Ext
2
+ module String
3
+ # Replace sequences of whitespace (including newlines) with either
4
+ # a single space or remove them entirely (according to param _spaced_).
5
+ #
6
+ # compress_lines(<<QUERY)
7
+ # SELECT name
8
+ # FROM users
9
+ # QUERY => "SELECT name FROM users"
10
+ #
11
+ # @param [String] string
12
+ # The input string.
13
+ #
14
+ # @param [TrueClass, FalseClass] spaced (default=true)
15
+ # Determines whether returned string has whitespace collapsed or removed.
16
+ #
17
+ # @return [String] The input string with whitespace (including newlines) replaced.
18
+ #
19
+ # @api semipublic
20
+ def self.compress_lines(string, spaced = true)
21
+ string.split($/).map { |line| line.strip }.join(spaced ? ' ' : '')
22
+ end
23
+ end
24
+ end; end
@@ -0,0 +1,12 @@
1
+ module Ardm
2
+ module Ext
3
+ def self.try_dup(value)
4
+ case value
5
+ when ::TrueClass, ::FalseClass, ::NilClass, ::Module, ::Numeric, ::Symbol
6
+ value
7
+ else
8
+ value.dup
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,405 @@
1
+ require 'active_support/concern'
2
+
3
+ module Ardm
4
+ #
5
+ # TODO: Write more documentation!
6
+ #
7
+ # Overview
8
+ # ========
9
+ #
10
+ # The Hook module is a very simple set of AOP helpers. Basically, it
11
+ # allows the developer to specify a method or block that should run
12
+ # before or after another method.
13
+ #
14
+ # Usage
15
+ # =====
16
+ #
17
+ # Halting The Hook Stack
18
+ #
19
+ # Inheritance
20
+ #
21
+ # Other Goodies
22
+ #
23
+ # Please bring up any issues regarding Hooks with carllerche on IRC
24
+ #
25
+ module Hook
26
+ extend ActiveSupport::Concern
27
+
28
+ included do
29
+ const_set("CLASS_HOOKS", {}) unless base.const_defined?("CLASS_HOOKS")
30
+ const_set("INSTANCE_HOOKS", {}) unless base.const_defined?("INSTANCE_HOOKS")
31
+
32
+ class << self
33
+ def method_added(name)
34
+ process_method_added(name, :instance)
35
+ super
36
+ end
37
+
38
+ def singleton_method_added(name)
39
+ process_method_added(name, :class)
40
+ super
41
+ end
42
+ end
43
+ end
44
+
45
+ module ClassMethods
46
+ extend Ardm::LocalObjectSpace
47
+ include Ardm::Assertions
48
+ # Inject code that executes before the target class method.
49
+ #
50
+ # @param target_method<Symbol> the name of the class method to inject before
51
+ # @param method_sym<Symbol> the name of the method to run before the
52
+ # target_method
53
+ # @param block<Block> the code to run before the target_method
54
+ #
55
+ # @note
56
+ # Either method_sym or block is required.
57
+ # -
58
+ # @api public
59
+ def before_class_method(target_method, method_sym = nil, &block)
60
+ install_hook :before, target_method, method_sym, :class, &block
61
+ end
62
+
63
+ #
64
+ # Inject code that executes after the target class method.
65
+ #
66
+ # @param target_method<Symbol> the name of the class method to inject after
67
+ # @param method_sym<Symbol> the name of the method to run after the target_method
68
+ # @param block<Block> the code to run after the target_method
69
+ #
70
+ # @note
71
+ # Either method_sym or block is required.
72
+ # -
73
+ # @api public
74
+ def after_class_method(target_method, method_sym = nil, &block)
75
+ install_hook :after, target_method, method_sym, :class, &block
76
+ end
77
+
78
+ #
79
+ # Inject code that executes before the target instance method.
80
+ #
81
+ # @param target_method<Symbol> the name of the instance method to inject before
82
+ # @param method_sym<Symbol> the name of the method to run before the
83
+ # target_method
84
+ # @param block<Block> the code to run before the target_method
85
+ #
86
+ # @note
87
+ # Either method_sym or block is required.
88
+ # -
89
+ # @api public
90
+ def before(target_method, method_sym = nil, &block)
91
+ install_hook :before, target_method, method_sym, :instance, &block
92
+ end
93
+
94
+ #
95
+ # Inject code that executes after the target instance method.
96
+ #
97
+ # @param target_method<Symbol> the name of the instance method to inject after
98
+ # @param method_sym<Symbol> the name of the method to run after the
99
+ # target_method
100
+ # @param block<Block> the code to run after the target_method
101
+ #
102
+ # @note
103
+ # Either method_sym or block is required.
104
+ # -
105
+ # @api public
106
+ def after(target_method, method_sym = nil, &block)
107
+ install_hook :after, target_method, method_sym, :instance, &block
108
+ end
109
+
110
+ # Register a class method as hookable. Registering a method means that
111
+ # before hooks will be run immediately before the method is invoked and
112
+ # after hooks will be called immediately after the method is invoked.
113
+ #
114
+ # @param hookable_method<Symbol> The name of the class method that should
115
+ # be hookable
116
+ # -
117
+ # @api public
118
+ def register_class_hooks(*hooks)
119
+ hooks.each { |hook| register_hook(hook, :class) }
120
+ end
121
+
122
+ # Register aninstance method as hookable. Registering a method means that
123
+ # before hooks will be run immediately before the method is invoked and
124
+ # after hooks will be called immediately after the method is invoked.
125
+ #
126
+ # @param hookable_method<Symbol> The name of the instance method that should
127
+ # be hookable
128
+ # -
129
+ # @api public
130
+ def register_instance_hooks(*hooks)
131
+ hooks.each { |hook| register_hook(hook, :instance) }
132
+ end
133
+
134
+ # Not yet implemented
135
+ def reset_hook!(target_method, scope)
136
+ raise NotImplementedError
137
+ end
138
+
139
+ # --- Alright kids... the rest is internal stuff ---
140
+
141
+ # Returns the correct HOOKS Hash depending on whether we are
142
+ # working with class methods or instance methods
143
+ def hooks_with_scope(scope)
144
+ case scope
145
+ when :class then class_hooks
146
+ when :instance then instance_hooks
147
+ else raise ArgumentError, 'You need to pass :class or :instance as scope'
148
+ end
149
+ end
150
+
151
+ def class_hooks
152
+ self.const_get("CLASS_HOOKS")
153
+ end
154
+
155
+ def instance_hooks
156
+ self.const_get("INSTANCE_HOOKS")
157
+ end
158
+
159
+ # Registers a method as hookable. Registering hooks involves the following
160
+ # process
161
+ #
162
+ # * Create a blank entry in the HOOK Hash for the method.
163
+ # * Define the methods that execute the before and after hook stack.
164
+ # These methods will be no-ops at first, but everytime a new hook is
165
+ # defined, the methods will be redefined to incorporate the new hook.
166
+ # * Redefine the method that is to be hookable so that the hook stacks
167
+ # are invoked approprietly.
168
+ def register_hook(target_method, scope)
169
+ if scope == :instance && !method_defined?(target_method)
170
+ raise ArgumentError, "#{target_method} instance method does not exist"
171
+ elsif scope == :class && !respond_to?(target_method)
172
+ raise ArgumentError, "#{target_method} class method does not exist"
173
+ end
174
+
175
+ hooks = hooks_with_scope(scope)
176
+
177
+ if hooks[target_method].nil?
178
+ hooks[target_method] = {
179
+ # We need to keep track of which class in the Inheritance chain the
180
+ # method was declared hookable in. Every time a child declares a new
181
+ # hook for the method, the hook stack invocations need to be redefined
182
+ # in the original Class. See #define_hook_stack_execution_methods
183
+ :before => [], :after => [], :in => self
184
+ }
185
+
186
+ define_hook_stack_execution_methods(target_method, scope)
187
+ define_advised_method(target_method, scope)
188
+ end
189
+ end
190
+
191
+ # Is the method registered as a hookable in the given scope.
192
+ def registered_as_hook?(target_method, scope)
193
+ ! hooks_with_scope(scope)[target_method].nil?
194
+ end
195
+
196
+ # Generates names for the various utility methods. We need to do this because
197
+ # the various utility methods should not end in = so, while we're at it, we
198
+ # might as well get rid of all punctuation.
199
+ def hook_method_name(target_method, prefix, suffix)
200
+ target_method = target_method.to_s
201
+
202
+ case target_method[-1,1]
203
+ when '?' then "#{prefix}_#{target_method[0..-2]}_ques_#{suffix}"
204
+ when '!' then "#{prefix}_#{target_method[0..-2]}_bang_#{suffix}"
205
+ when '=' then "#{prefix}_#{target_method[0..-2]}_eq_#{suffix}"
206
+ # I add a _nan_ suffix here so that we don't ever encounter
207
+ # any naming conflicts.
208
+ else "#{prefix}_#{target_method[0..-1]}_nan_#{suffix}"
209
+ end
210
+ end
211
+
212
+ # This will need to be refactored
213
+ def process_method_added(method_name, scope)
214
+ hooks_with_scope(scope).each do |target_method, hooks|
215
+ if hooks[:before].any? { |hook| hook[:name] == method_name }
216
+ define_hook_stack_execution_methods(target_method, scope)
217
+ end
218
+
219
+ if hooks[:after].any? { |hook| hook[:name] == method_name }
220
+ define_hook_stack_execution_methods(target_method, scope)
221
+ end
222
+ end
223
+ end
224
+
225
+ # Defines two methods. One method executes the before hook stack. The other executes
226
+ # the after hook stack. This method will be called many times during the Class definition
227
+ # process. It should be called for each hook that is defined. It will also be called
228
+ # when a hook is redefined (to make sure that the arity hasn't changed).
229
+ def define_hook_stack_execution_methods(target_method, scope)
230
+ unless registered_as_hook?(target_method, scope)
231
+ raise ArgumentError, "#{target_method} has not be registered as a hookable #{scope} method"
232
+ end
233
+
234
+ hooks = hooks_with_scope(scope)
235
+
236
+ before_hooks = hooks[target_method][:before]
237
+ before_hooks = before_hooks.map{ |info| inline_call(info, scope) }.join("\n")
238
+
239
+ after_hooks = hooks[target_method][:after]
240
+ after_hooks = after_hooks.map{ |info| inline_call(info, scope) }.join("\n")
241
+
242
+ before_hook_name = hook_method_name(target_method, 'execute_before', 'hook_stack')
243
+ after_hook_name = hook_method_name(target_method, 'execute_after', 'hook_stack')
244
+
245
+ hooks[target_method][:in].class_eval <<-RUBY, __FILE__, __LINE__ + 1
246
+ #{scope == :class ? 'class << self' : ''}
247
+
248
+ private
249
+
250
+ remove_method :#{before_hook_name} if instance_methods(false).any? { |m| m.to_sym == :#{before_hook_name} }
251
+ def #{before_hook_name}(*args)
252
+ #{before_hooks}
253
+ end
254
+
255
+ remove_method :#{after_hook_name} if instance_methods(false).any? { |m| m.to_sym == :#{after_hook_name} }
256
+ def #{after_hook_name}(*args)
257
+ #{after_hooks}
258
+ end
259
+
260
+ #{scope == :class ? 'end' : ''}
261
+ RUBY
262
+ end
263
+
264
+ # Returns ruby code that will invoke the hook. It checks the arity of the hook method
265
+ # and passes arguments accordingly.
266
+ def inline_call(method_info, scope)
267
+ Ardm::Hook::ClassMethods.hook_scopes << method_info[:from]
268
+ name = method_info[:name]
269
+ if scope == :instance
270
+ args = method_defined?(name) && instance_method(name).arity != 0 ? '*args' : ''
271
+ %(#{name}(#{args}) if self.class <= Ardm::Hook::ClassMethods.object_by_id(#{method_info[:from].object_id}))
272
+ else
273
+ args = respond_to?(name) && method(name).arity != 0 ? '*args' : ''
274
+ %(#{name}(#{args}) if self <= Ardm::Hook::ClassMethods.object_by_id(#{method_info[:from].object_id}))
275
+ end
276
+ end
277
+
278
+ def define_advised_method(target_method, scope)
279
+ args = args_for(method_with_scope(target_method, scope))
280
+
281
+ renamed_target = hook_method_name(target_method, 'hookable_', 'before_advised')
282
+
283
+ source = <<-EOD
284
+ def #{target_method}(#{args})
285
+ retval = nil
286
+ catch(:halt) do
287
+ #{hook_method_name(target_method, 'execute_before', 'hook_stack')}(#{args})
288
+ retval = #{renamed_target}(#{args})
289
+ #{hook_method_name(target_method, 'execute_after', 'hook_stack')}(retval, #{args})
290
+ retval
291
+ end
292
+ end
293
+ EOD
294
+
295
+ if scope == :instance && !instance_methods(false).any? { |m| m.to_sym == target_method }
296
+ send(:alias_method, renamed_target, target_method)
297
+
298
+ proxy_module = Module.new
299
+ proxy_module.class_eval(source, __FILE__, __LINE__)
300
+ self.send(:include, proxy_module)
301
+ else
302
+ source = %{alias_method :#{renamed_target}, :#{target_method}\n#{source}}
303
+ source = %{class << self\n#{source}\nend} if scope == :class
304
+ class_eval(source, __FILE__, __LINE__)
305
+ end
306
+ end
307
+
308
+ # --- Add a hook ---
309
+
310
+ def install_hook(type, target_method, method_sym, scope, &block)
311
+ assert_kind_of 'target_method', target_method, Symbol
312
+ assert_kind_of 'method_sym', method_sym, Symbol unless method_sym.nil?
313
+ assert_kind_of 'scope', scope, Symbol
314
+
315
+ if !block_given? and method_sym.nil?
316
+ raise ArgumentError, "You need to pass 2 arguments to \"#{type}\"."
317
+ end
318
+
319
+ if method_sym.to_s[-1,1] == '='
320
+ raise ArgumentError, "Methods ending in = cannot be hooks"
321
+ end
322
+
323
+ unless [ :class, :instance ].include?(scope)
324
+ raise ArgumentError, 'You need to pass :class or :instance as scope'
325
+ end
326
+
327
+ if registered_as_hook?(target_method, scope)
328
+ hooks = hooks_with_scope(scope)
329
+
330
+ #if this hook is previously declared in a sibling or cousin we must move the :in class
331
+ #to the common ancestor to get both hooks to run.
332
+ if !(hooks[target_method][:in] <=> self)
333
+ before_hook_name = hook_method_name(target_method, 'execute_before', 'hook_stack')
334
+ after_hook_name = hook_method_name(target_method, 'execute_after', 'hook_stack')
335
+
336
+ hooks[target_method][:in].class_eval <<-RUBY, __FILE__, __LINE__ + 1
337
+ remove_method :#{before_hook_name} if instance_methods(false).any? { |m| m.to_sym == :#{before_hook_name} }
338
+ def #{before_hook_name}(*args)
339
+ super
340
+ end
341
+
342
+ remove_method :#{after_hook_name} if instance_methods(false).any? { |m| m.to_sym == :#{before_hook_name} }
343
+ def #{after_hook_name}(*args)
344
+ super
345
+ end
346
+ RUBY
347
+
348
+ while !(hooks[target_method][:in] <=> self) do
349
+ hooks[target_method][:in] = hooks[target_method][:in].superclass
350
+ end
351
+
352
+ define_hook_stack_execution_methods(target_method, scope)
353
+ hooks[target_method][:in].class_eval{define_advised_method(target_method, scope)}
354
+ end
355
+ else
356
+ register_hook(target_method, scope)
357
+ hooks = hooks_with_scope(scope)
358
+ end
359
+
360
+ #if we were passed a block, create a method out of it.
361
+ if block
362
+ method_sym = "__hooks_#{type}_#{quote_method(target_method)}_#{hooks[target_method][type].length}".to_sym
363
+ if scope == :class
364
+ singleton_class.instance_eval do
365
+ define_method(method_sym, &block)
366
+ end
367
+ else
368
+ define_method(method_sym, &block)
369
+ end
370
+ end
371
+
372
+ # Adds method to the stack an redefines the hook invocation method
373
+ hooks[target_method][type] << { :name => method_sym, :from => self }
374
+ define_hook_stack_execution_methods(target_method, scope)
375
+ end
376
+
377
+ # --- Helpers ---
378
+
379
+ def args_for(method)
380
+ if method.arity == 0
381
+ "&block"
382
+ elsif method.arity > 0
383
+ "_" << (1 .. method.arity).to_a.join(", _") << ", &block"
384
+ elsif (method.arity + 1) < 0
385
+ "_" << (1 .. (method.arity).abs - 1).to_a.join(", _") << ", *args, &block"
386
+ else
387
+ "*args, &block"
388
+ end
389
+ end
390
+
391
+ def method_with_scope(name, scope)
392
+ case scope
393
+ when :class then method(name)
394
+ when :instance then instance_method(name)
395
+ else raise ArgumentError, 'You need to pass :class or :instance as scope'
396
+ end
397
+ end
398
+
399
+ def quote_method(name)
400
+ name.to_s.gsub(/\?$/, '_q_').gsub(/!$/, '_b_').gsub(/=$/, '_eq_')
401
+ end
402
+ end
403
+
404
+ end
405
+ end