core_ext 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (175) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +3 -0
  3. data/lib/core_ext/array/access.rb +76 -0
  4. data/lib/core_ext/array/conversions.rb +211 -0
  5. data/lib/core_ext/array/extract_options.rb +29 -0
  6. data/lib/core_ext/array/grouping.rb +116 -0
  7. data/lib/core_ext/array/inquiry.rb +17 -0
  8. data/lib/core_ext/array/prepend_and_append.rb +7 -0
  9. data/lib/core_ext/array/wrap.rb +46 -0
  10. data/lib/core_ext/array.rb +7 -0
  11. data/lib/core_ext/array_inquirer.rb +44 -0
  12. data/lib/core_ext/benchmark.rb +14 -0
  13. data/lib/core_ext/benchmarkable.rb +49 -0
  14. data/lib/core_ext/big_decimal/conversions.rb +14 -0
  15. data/lib/core_ext/big_decimal.rb +1 -0
  16. data/lib/core_ext/builder.rb +6 -0
  17. data/lib/core_ext/callbacks.rb +770 -0
  18. data/lib/core_ext/class/attribute.rb +128 -0
  19. data/lib/core_ext/class/attribute_accessors.rb +4 -0
  20. data/lib/core_ext/class/subclasses.rb +42 -0
  21. data/lib/core_ext/class.rb +2 -0
  22. data/lib/core_ext/concern.rb +142 -0
  23. data/lib/core_ext/configurable.rb +148 -0
  24. data/lib/core_ext/date/acts_like.rb +8 -0
  25. data/lib/core_ext/date/blank.rb +12 -0
  26. data/lib/core_ext/date/calculations.rb +143 -0
  27. data/lib/core_ext/date/conversions.rb +93 -0
  28. data/lib/core_ext/date/zones.rb +6 -0
  29. data/lib/core_ext/date.rb +5 -0
  30. data/lib/core_ext/date_and_time/calculations.rb +328 -0
  31. data/lib/core_ext/date_and_time/zones.rb +40 -0
  32. data/lib/core_ext/date_time/acts_like.rb +14 -0
  33. data/lib/core_ext/date_time/blank.rb +12 -0
  34. data/lib/core_ext/date_time/calculations.rb +177 -0
  35. data/lib/core_ext/date_time/conversions.rb +104 -0
  36. data/lib/core_ext/date_time/zones.rb +6 -0
  37. data/lib/core_ext/date_time.rb +5 -0
  38. data/lib/core_ext/deprecation/behaviors.rb +86 -0
  39. data/lib/core_ext/deprecation/instance_delegator.rb +24 -0
  40. data/lib/core_ext/deprecation/method_wrappers.rb +70 -0
  41. data/lib/core_ext/deprecation/proxy_wrappers.rb +149 -0
  42. data/lib/core_ext/deprecation/reporting.rb +105 -0
  43. data/lib/core_ext/deprecation.rb +43 -0
  44. data/lib/core_ext/digest/uuid.rb +51 -0
  45. data/lib/core_ext/duration.rb +157 -0
  46. data/lib/core_ext/enumerable.rb +106 -0
  47. data/lib/core_ext/file/atomic.rb +68 -0
  48. data/lib/core_ext/file.rb +1 -0
  49. data/lib/core_ext/hash/compact.rb +20 -0
  50. data/lib/core_ext/hash/conversions.rb +261 -0
  51. data/lib/core_ext/hash/deep_merge.rb +38 -0
  52. data/lib/core_ext/hash/except.rb +22 -0
  53. data/lib/core_ext/hash/indifferent_access.rb +23 -0
  54. data/lib/core_ext/hash/keys.rb +170 -0
  55. data/lib/core_ext/hash/reverse_merge.rb +22 -0
  56. data/lib/core_ext/hash/slice.rb +48 -0
  57. data/lib/core_ext/hash/transform_values.rb +29 -0
  58. data/lib/core_ext/hash.rb +9 -0
  59. data/lib/core_ext/hash_with_indifferent_access.rb +298 -0
  60. data/lib/core_ext/inflections.rb +70 -0
  61. data/lib/core_ext/inflector/inflections.rb +244 -0
  62. data/lib/core_ext/inflector/methods.rb +381 -0
  63. data/lib/core_ext/inflector/transliterate.rb +112 -0
  64. data/lib/core_ext/inflector.rb +7 -0
  65. data/lib/core_ext/integer/inflections.rb +29 -0
  66. data/lib/core_ext/integer/multiple.rb +10 -0
  67. data/lib/core_ext/integer/time.rb +29 -0
  68. data/lib/core_ext/integer.rb +3 -0
  69. data/lib/core_ext/json/decoding.rb +67 -0
  70. data/lib/core_ext/json/encoding.rb +127 -0
  71. data/lib/core_ext/json.rb +2 -0
  72. data/lib/core_ext/kernel/agnostics.rb +11 -0
  73. data/lib/core_ext/kernel/concern.rb +10 -0
  74. data/lib/core_ext/kernel/reporting.rb +41 -0
  75. data/lib/core_ext/kernel/singleton_class.rb +6 -0
  76. data/lib/core_ext/kernel.rb +4 -0
  77. data/lib/core_ext/load_error.rb +30 -0
  78. data/lib/core_ext/logger.rb +57 -0
  79. data/lib/core_ext/logger_silence.rb +24 -0
  80. data/lib/core_ext/marshal.rb +19 -0
  81. data/lib/core_ext/module/aliasing.rb +74 -0
  82. data/lib/core_ext/module/anonymous.rb +28 -0
  83. data/lib/core_ext/module/attr_internal.rb +36 -0
  84. data/lib/core_ext/module/attribute_accessors.rb +212 -0
  85. data/lib/core_ext/module/concerning.rb +135 -0
  86. data/lib/core_ext/module/delegation.rb +218 -0
  87. data/lib/core_ext/module/deprecation.rb +23 -0
  88. data/lib/core_ext/module/introspection.rb +62 -0
  89. data/lib/core_ext/module/method_transplanting.rb +3 -0
  90. data/lib/core_ext/module/qualified_const.rb +52 -0
  91. data/lib/core_ext/module/reachable.rb +8 -0
  92. data/lib/core_ext/module/remove_method.rb +35 -0
  93. data/lib/core_ext/module.rb +11 -0
  94. data/lib/core_ext/multibyte/chars.rb +231 -0
  95. data/lib/core_ext/multibyte/unicode.rb +388 -0
  96. data/lib/core_ext/multibyte.rb +21 -0
  97. data/lib/core_ext/name_error.rb +31 -0
  98. data/lib/core_ext/numeric/bytes.rb +64 -0
  99. data/lib/core_ext/numeric/conversions.rb +132 -0
  100. data/lib/core_ext/numeric/inquiry.rb +26 -0
  101. data/lib/core_ext/numeric/time.rb +74 -0
  102. data/lib/core_ext/numeric.rb +4 -0
  103. data/lib/core_ext/object/acts_like.rb +10 -0
  104. data/lib/core_ext/object/blank.rb +140 -0
  105. data/lib/core_ext/object/conversions.rb +4 -0
  106. data/lib/core_ext/object/deep_dup.rb +53 -0
  107. data/lib/core_ext/object/duplicable.rb +98 -0
  108. data/lib/core_ext/object/inclusion.rb +27 -0
  109. data/lib/core_ext/object/instance_variables.rb +28 -0
  110. data/lib/core_ext/object/json.rb +199 -0
  111. data/lib/core_ext/object/to_param.rb +1 -0
  112. data/lib/core_ext/object/to_query.rb +84 -0
  113. data/lib/core_ext/object/try.rb +146 -0
  114. data/lib/core_ext/object/with_options.rb +69 -0
  115. data/lib/core_ext/object.rb +14 -0
  116. data/lib/core_ext/option_merger.rb +25 -0
  117. data/lib/core_ext/ordered_hash.rb +48 -0
  118. data/lib/core_ext/ordered_options.rb +81 -0
  119. data/lib/core_ext/range/conversions.rb +34 -0
  120. data/lib/core_ext/range/each.rb +21 -0
  121. data/lib/core_ext/range/include_range.rb +23 -0
  122. data/lib/core_ext/range/overlaps.rb +8 -0
  123. data/lib/core_ext/range.rb +4 -0
  124. data/lib/core_ext/regexp.rb +5 -0
  125. data/lib/core_ext/rescuable.rb +119 -0
  126. data/lib/core_ext/securerandom.rb +23 -0
  127. data/lib/core_ext/security_utils.rb +20 -0
  128. data/lib/core_ext/string/access.rb +104 -0
  129. data/lib/core_ext/string/behavior.rb +6 -0
  130. data/lib/core_ext/string/conversions.rb +56 -0
  131. data/lib/core_ext/string/exclude.rb +11 -0
  132. data/lib/core_ext/string/filters.rb +102 -0
  133. data/lib/core_ext/string/indent.rb +43 -0
  134. data/lib/core_ext/string/inflections.rb +235 -0
  135. data/lib/core_ext/string/inquiry.rb +13 -0
  136. data/lib/core_ext/string/multibyte.rb +53 -0
  137. data/lib/core_ext/string/output_safety.rb +261 -0
  138. data/lib/core_ext/string/starts_ends_with.rb +4 -0
  139. data/lib/core_ext/string/strip.rb +23 -0
  140. data/lib/core_ext/string/zones.rb +14 -0
  141. data/lib/core_ext/string.rb +13 -0
  142. data/lib/core_ext/string_inquirer.rb +26 -0
  143. data/lib/core_ext/tagged_logging.rb +78 -0
  144. data/lib/core_ext/test_case.rb +88 -0
  145. data/lib/core_ext/testing/assertions.rb +99 -0
  146. data/lib/core_ext/testing/autorun.rb +12 -0
  147. data/lib/core_ext/testing/composite_filter.rb +54 -0
  148. data/lib/core_ext/testing/constant_lookup.rb +50 -0
  149. data/lib/core_ext/testing/declarative.rb +26 -0
  150. data/lib/core_ext/testing/deprecation.rb +36 -0
  151. data/lib/core_ext/testing/file_fixtures.rb +34 -0
  152. data/lib/core_ext/testing/isolation.rb +115 -0
  153. data/lib/core_ext/testing/method_call_assertions.rb +41 -0
  154. data/lib/core_ext/testing/setup_and_teardown.rb +50 -0
  155. data/lib/core_ext/testing/stream.rb +42 -0
  156. data/lib/core_ext/testing/tagged_logging.rb +25 -0
  157. data/lib/core_ext/testing/time_helpers.rb +134 -0
  158. data/lib/core_ext/time/acts_like.rb +8 -0
  159. data/lib/core_ext/time/calculations.rb +284 -0
  160. data/lib/core_ext/time/conversions.rb +66 -0
  161. data/lib/core_ext/time/zones.rb +95 -0
  162. data/lib/core_ext/time.rb +20 -0
  163. data/lib/core_ext/time_with_zone.rb +503 -0
  164. data/lib/core_ext/time_zone.rb +464 -0
  165. data/lib/core_ext/uri.rb +25 -0
  166. data/lib/core_ext/version.rb +3 -0
  167. data/lib/core_ext/xml_mini/jdom.rb +181 -0
  168. data/lib/core_ext/xml_mini/libxml.rb +79 -0
  169. data/lib/core_ext/xml_mini/libxmlsax.rb +85 -0
  170. data/lib/core_ext/xml_mini/nokogiri.rb +83 -0
  171. data/lib/core_ext/xml_mini/nokogirisax.rb +87 -0
  172. data/lib/core_ext/xml_mini/rexml.rb +130 -0
  173. data/lib/core_ext/xml_mini.rb +194 -0
  174. data/lib/core_ext.rb +3 -0
  175. metadata +310 -0
@@ -0,0 +1,770 @@
1
+ require 'core_ext/concern'
2
+ require 'core_ext/array/extract_options'
3
+ require 'core_ext/class/attribute'
4
+ require 'core_ext/kernel/reporting'
5
+ require 'core_ext/kernel/singleton_class'
6
+ require 'core_ext/module/attribute_accessors'
7
+ require 'core_ext/string/filters'
8
+ require 'core_ext/deprecation'
9
+ require 'thread'
10
+
11
+ module CoreExt
12
+ # Callbacks are code hooks that are run at key points in an object's life cycle.
13
+ # The typical use case is to have a base class define a set of callbacks
14
+ # relevant to the other functionality it supplies, so that subclasses can
15
+ # install callbacks that enhance or modify the base functionality without
16
+ # needing to override or redefine methods of the base class.
17
+ #
18
+ # Mixing in this module allows you to define the events in the object's
19
+ # life cycle that will support callbacks (via +ClassMethods.define_callbacks+),
20
+ # set the instance methods, procs, or callback objects to be called (via
21
+ # +ClassMethods.set_callback+), and run the installed callbacks at the
22
+ # appropriate times (via +run_callbacks+).
23
+ #
24
+ # Three kinds of callbacks are supported: before callbacks, run before a
25
+ # certain event; after callbacks, run after the event; and around callbacks,
26
+ # blocks that surround the event, triggering it when they yield. Callback code
27
+ # can be contained in instance methods, procs or lambdas, or callback objects
28
+ # that respond to certain predetermined methods. See +ClassMethods.set_callback+
29
+ # for details.
30
+ #
31
+ # class Record
32
+ # include CoreExt::Callbacks
33
+ # define_callbacks :save
34
+ #
35
+ # def save
36
+ # run_callbacks :save do
37
+ # puts "- save"
38
+ # end
39
+ # end
40
+ # end
41
+ #
42
+ # class PersonRecord < Record
43
+ # set_callback :save, :before, :saving_message
44
+ # def saving_message
45
+ # puts "saving..."
46
+ # end
47
+ #
48
+ # set_callback :save, :after do |object|
49
+ # puts "saved"
50
+ # end
51
+ # end
52
+ #
53
+ # person = PersonRecord.new
54
+ # person.save
55
+ #
56
+ # Output:
57
+ # saving...
58
+ # - save
59
+ # saved
60
+ module Callbacks
61
+ extend Concern
62
+
63
+ CALLBACK_FILTER_TYPES = [:before, :after, :around]
64
+
65
+ # If true, Active Record and Active Model callbacks returning +false+ will
66
+ # halt the entire callback chain and display a deprecation message.
67
+ # If false, callback chains will only be halted by calling +throw :abort+.
68
+ # Defaults to +true+.
69
+ mattr_accessor(:halt_and_display_warning_on_return_false) { true }
70
+
71
+ # Runs the callbacks for the given event.
72
+ #
73
+ # Calls the before and around callbacks in the order they were set, yields
74
+ # the block (if given one), and then runs the after callbacks in reverse
75
+ # order.
76
+ #
77
+ # If the callback chain was halted, returns +false+. Otherwise returns the
78
+ # result of the block, +nil+ if no callbacks have been set, or +true+
79
+ # if callbacks have been set but no block is given.
80
+ #
81
+ # run_callbacks :save do
82
+ # save
83
+ # end
84
+ def run_callbacks(kind, &block)
85
+ send "_run_#{kind}_callbacks", &block
86
+ end
87
+
88
+ private
89
+
90
+ def __run_callbacks__(callbacks, &block)
91
+ if callbacks.empty?
92
+ yield if block_given?
93
+ else
94
+ runner = callbacks.compile
95
+ e = Filters::Environment.new(self, false, nil, block)
96
+ runner.call(e).value
97
+ end
98
+ end
99
+
100
+ # A hook invoked every time a before callback is halted.
101
+ # This can be overridden in AS::Callback implementors in order
102
+ # to provide better debugging/logging.
103
+ def halted_callback_hook(filter)
104
+ end
105
+
106
+ module Conditionals # :nodoc:
107
+ class Value
108
+ def initialize(&block)
109
+ @block = block
110
+ end
111
+ def call(target, value); @block.call(value); end
112
+ end
113
+ end
114
+
115
+ module Filters
116
+ Environment = Struct.new(:target, :halted, :value, :run_block)
117
+
118
+ class End
119
+ def call(env)
120
+ block = env.run_block
121
+ env.value = !env.halted && (!block || block.call)
122
+ env
123
+ end
124
+ end
125
+ ENDING = End.new
126
+
127
+ class Before
128
+ def self.build(callback_sequence, user_callback, user_conditions, chain_config, filter)
129
+ halted_lambda = chain_config[:terminator]
130
+
131
+ if user_conditions.any?
132
+ halting_and_conditional(callback_sequence, user_callback, user_conditions, halted_lambda, filter)
133
+ else
134
+ halting(callback_sequence, user_callback, halted_lambda, filter)
135
+ end
136
+ end
137
+
138
+ def self.halting_and_conditional(callback_sequence, user_callback, user_conditions, halted_lambda, filter)
139
+ callback_sequence.before do |env|
140
+ target = env.target
141
+ value = env.value
142
+ halted = env.halted
143
+
144
+ if !halted && user_conditions.all? { |c| c.call(target, value) }
145
+ result_lambda = -> { user_callback.call target, value }
146
+ env.halted = halted_lambda.call(target, result_lambda)
147
+ if env.halted
148
+ target.send :halted_callback_hook, filter
149
+ end
150
+ end
151
+
152
+ env
153
+ end
154
+ end
155
+ private_class_method :halting_and_conditional
156
+
157
+ def self.halting(callback_sequence, user_callback, halted_lambda, filter)
158
+ callback_sequence.before do |env|
159
+ target = env.target
160
+ value = env.value
161
+ halted = env.halted
162
+
163
+ unless halted
164
+ result_lambda = -> { user_callback.call target, value }
165
+ env.halted = halted_lambda.call(target, result_lambda)
166
+
167
+ if env.halted
168
+ target.send :halted_callback_hook, filter
169
+ end
170
+ end
171
+
172
+ env
173
+ end
174
+ end
175
+ private_class_method :halting
176
+ end
177
+
178
+ class After
179
+ def self.build(callback_sequence, user_callback, user_conditions, chain_config)
180
+ if chain_config[:skip_after_callbacks_if_terminated]
181
+ if user_conditions.any?
182
+ halting_and_conditional(callback_sequence, user_callback, user_conditions)
183
+ else
184
+ halting(callback_sequence, user_callback)
185
+ end
186
+ else
187
+ if user_conditions.any?
188
+ conditional callback_sequence, user_callback, user_conditions
189
+ else
190
+ simple callback_sequence, user_callback
191
+ end
192
+ end
193
+ end
194
+
195
+ def self.halting_and_conditional(callback_sequence, user_callback, user_conditions)
196
+ callback_sequence.after do |env|
197
+ target = env.target
198
+ value = env.value
199
+ halted = env.halted
200
+
201
+ if !halted && user_conditions.all? { |c| c.call(target, value) }
202
+ user_callback.call target, value
203
+ end
204
+
205
+ env
206
+ end
207
+ end
208
+ private_class_method :halting_and_conditional
209
+
210
+ def self.halting(callback_sequence, user_callback)
211
+ callback_sequence.after do |env|
212
+ unless env.halted
213
+ user_callback.call env.target, env.value
214
+ end
215
+
216
+ env
217
+ end
218
+ end
219
+ private_class_method :halting
220
+
221
+ def self.conditional(callback_sequence, user_callback, user_conditions)
222
+ callback_sequence.after do |env|
223
+ target = env.target
224
+ value = env.value
225
+
226
+ if user_conditions.all? { |c| c.call(target, value) }
227
+ user_callback.call target, value
228
+ end
229
+
230
+ env
231
+ end
232
+ end
233
+ private_class_method :conditional
234
+
235
+ def self.simple(callback_sequence, user_callback)
236
+ callback_sequence.after do |env|
237
+ user_callback.call env.target, env.value
238
+
239
+ env
240
+ end
241
+ end
242
+ private_class_method :simple
243
+ end
244
+
245
+ class Around
246
+ def self.build(callback_sequence, user_callback, user_conditions, chain_config)
247
+ if user_conditions.any?
248
+ halting_and_conditional(callback_sequence, user_callback, user_conditions)
249
+ else
250
+ halting(callback_sequence, user_callback)
251
+ end
252
+ end
253
+
254
+ def self.halting_and_conditional(callback_sequence, user_callback, user_conditions)
255
+ callback_sequence.around do |env, &run|
256
+ target = env.target
257
+ value = env.value
258
+ halted = env.halted
259
+
260
+ if !halted && user_conditions.all? { |c| c.call(target, value) }
261
+ user_callback.call(target, value) {
262
+ run.call.value
263
+ }
264
+ env
265
+ else
266
+ run.call
267
+ end
268
+ end
269
+ end
270
+ private_class_method :halting_and_conditional
271
+
272
+ def self.halting(callback_sequence, user_callback)
273
+ callback_sequence.around do |env, &run|
274
+ target = env.target
275
+ value = env.value
276
+
277
+ if env.halted
278
+ run.call
279
+ else
280
+ user_callback.call(target, value) {
281
+ run.call.value
282
+ }
283
+ env
284
+ end
285
+ end
286
+ end
287
+ private_class_method :halting
288
+ end
289
+ end
290
+
291
+ class Callback #:nodoc:#
292
+ def self.build(chain, filter, kind, options)
293
+ new chain.name, filter, kind, options, chain.config
294
+ end
295
+
296
+ attr_accessor :kind, :name
297
+ attr_reader :chain_config
298
+
299
+ def initialize(name, filter, kind, options, chain_config)
300
+ @chain_config = chain_config
301
+ @name = name
302
+ @kind = kind
303
+ @filter = filter
304
+ @key = compute_identifier filter
305
+ @if = Array(options[:if])
306
+ @unless = Array(options[:unless])
307
+ end
308
+
309
+ def filter; @key; end
310
+ def raw_filter; @filter; end
311
+
312
+ def merge_conditional_options(chain, if_option:, unless_option:)
313
+ options = {
314
+ :if => @if.dup,
315
+ :unless => @unless.dup
316
+ }
317
+
318
+ options[:if].concat Array(unless_option)
319
+ options[:unless].concat Array(if_option)
320
+
321
+ self.class.build chain, @filter, @kind, options
322
+ end
323
+
324
+ def matches?(_kind, _filter)
325
+ @kind == _kind && filter == _filter
326
+ end
327
+
328
+ def duplicates?(other)
329
+ case @filter
330
+ when Symbol, String
331
+ matches?(other.kind, other.filter)
332
+ else
333
+ false
334
+ end
335
+ end
336
+
337
+ # Wraps code with filter
338
+ def apply(callback_sequence)
339
+ user_conditions = conditions_lambdas
340
+ user_callback = make_lambda @filter
341
+
342
+ case kind
343
+ when :before
344
+ Filters::Before.build(callback_sequence, user_callback, user_conditions, chain_config, @filter)
345
+ when :after
346
+ Filters::After.build(callback_sequence, user_callback, user_conditions, chain_config)
347
+ when :around
348
+ Filters::Around.build(callback_sequence, user_callback, user_conditions, chain_config)
349
+ end
350
+ end
351
+
352
+ private
353
+
354
+ def invert_lambda(l)
355
+ lambda { |*args, &blk| !l.call(*args, &blk) }
356
+ end
357
+
358
+ # Filters support:
359
+ #
360
+ # Symbols:: A method to call.
361
+ # Strings:: Some content to evaluate.
362
+ # Procs:: A proc to call with the object.
363
+ # Objects:: An object with a <tt>before_foo</tt> method on it to call.
364
+ #
365
+ # All of these objects are converted into a lambda and handled
366
+ # the same after this point.
367
+ def make_lambda(filter)
368
+ case filter
369
+ when Symbol
370
+ lambda { |target, _, &blk| target.send filter, &blk }
371
+ when String
372
+ l = eval "lambda { |value| #{filter} }"
373
+ lambda { |target, value| target.instance_exec(value, &l) }
374
+ when Conditionals::Value then filter
375
+ when ::Proc
376
+ if filter.arity > 1
377
+ return lambda { |target, _, &block|
378
+ raise ArgumentError unless block
379
+ target.instance_exec(target, block, &filter)
380
+ }
381
+ end
382
+
383
+ if filter.arity <= 0
384
+ lambda { |target, _| target.instance_exec(&filter) }
385
+ else
386
+ lambda { |target, _| target.instance_exec(target, &filter) }
387
+ end
388
+ else
389
+ scopes = Array(chain_config[:scope])
390
+ method_to_call = scopes.map{ |s| public_send(s) }.join("_")
391
+
392
+ lambda { |target, _, &blk|
393
+ filter.public_send method_to_call, target, &blk
394
+ }
395
+ end
396
+ end
397
+
398
+ def compute_identifier(filter)
399
+ case filter
400
+ when String, ::Proc
401
+ filter.object_id
402
+ else
403
+ filter
404
+ end
405
+ end
406
+
407
+ def conditions_lambdas
408
+ @if.map { |c| make_lambda c } +
409
+ @unless.map { |c| invert_lambda make_lambda c }
410
+ end
411
+ end
412
+
413
+ # Execute before and after filters in a sequence instead of
414
+ # chaining them with nested lambda calls, see:
415
+ # https://github.com/rails/rails/issues/18011
416
+ class CallbackSequence
417
+ def initialize(&call)
418
+ @call = call
419
+ @before = []
420
+ @after = []
421
+ end
422
+
423
+ def before(&before)
424
+ @before.unshift(before)
425
+ self
426
+ end
427
+
428
+ def after(&after)
429
+ @after.push(after)
430
+ self
431
+ end
432
+
433
+ def around(&around)
434
+ CallbackSequence.new do |arg|
435
+ around.call(arg) {
436
+ self.call(arg)
437
+ }
438
+ end
439
+ end
440
+
441
+ def call(arg)
442
+ @before.each { |b| b.call(arg) }
443
+ value = @call.call(arg)
444
+ @after.each { |a| a.call(arg) }
445
+ value
446
+ end
447
+ end
448
+
449
+ # An Array with a compile method.
450
+ class CallbackChain #:nodoc:#
451
+ include Enumerable
452
+
453
+ attr_reader :name, :config
454
+
455
+ def initialize(name, config)
456
+ @name = name
457
+ @config = {
458
+ scope: [:kind],
459
+ terminator: default_terminator
460
+ }.merge!(config)
461
+ @chain = []
462
+ @callbacks = nil
463
+ @mutex = Mutex.new
464
+ end
465
+
466
+ def each(&block); @chain.each(&block); end
467
+ def index(o); @chain.index(o); end
468
+ def empty?; @chain.empty?; end
469
+
470
+ def insert(index, o)
471
+ @callbacks = nil
472
+ @chain.insert(index, o)
473
+ end
474
+
475
+ def delete(o)
476
+ @callbacks = nil
477
+ @chain.delete(o)
478
+ end
479
+
480
+ def clear
481
+ @callbacks = nil
482
+ @chain.clear
483
+ self
484
+ end
485
+
486
+ def initialize_copy(other)
487
+ @callbacks = nil
488
+ @chain = other.chain.dup
489
+ @mutex = Mutex.new
490
+ end
491
+
492
+ def compile
493
+ @callbacks || @mutex.synchronize do
494
+ final_sequence = CallbackSequence.new { |env| Filters::ENDING.call(env) }
495
+ @callbacks ||= @chain.reverse.inject(final_sequence) do |callback_sequence, callback|
496
+ callback.apply callback_sequence
497
+ end
498
+ end
499
+ end
500
+
501
+ def append(*callbacks)
502
+ callbacks.each { |c| append_one(c) }
503
+ end
504
+
505
+ def prepend(*callbacks)
506
+ callbacks.each { |c| prepend_one(c) }
507
+ end
508
+
509
+ protected
510
+ def chain; @chain; end
511
+
512
+ private
513
+
514
+ def append_one(callback)
515
+ @callbacks = nil
516
+ remove_duplicates(callback)
517
+ @chain.push(callback)
518
+ end
519
+
520
+ def prepend_one(callback)
521
+ @callbacks = nil
522
+ remove_duplicates(callback)
523
+ @chain.unshift(callback)
524
+ end
525
+
526
+ def remove_duplicates(callback)
527
+ @callbacks = nil
528
+ @chain.delete_if { |c| callback.duplicates?(c) }
529
+ end
530
+
531
+ def default_terminator
532
+ Proc.new do |target, result_lambda|
533
+ terminate = true
534
+ catch(:abort) do
535
+ result_lambda.call if result_lambda.is_a?(Proc)
536
+ terminate = false
537
+ end
538
+ terminate
539
+ end
540
+ end
541
+ end
542
+
543
+ module ClassMethods
544
+ def normalize_callback_params(filters, block) # :nodoc:
545
+ type = CALLBACK_FILTER_TYPES.include?(filters.first) ? filters.shift : :before
546
+ options = filters.extract_options!
547
+ filters.unshift(block) if block
548
+ [type, filters, options.dup]
549
+ end
550
+
551
+ # This is used internally to append, prepend and skip callbacks to the
552
+ # CallbackChain.
553
+ def __update_callbacks(name) #:nodoc:
554
+ chain = get_callbacks name
555
+ yield self, chain.dup
556
+ end
557
+
558
+ # Install a callback for the given event.
559
+ #
560
+ # set_callback :save, :before, :before_meth
561
+ # set_callback :save, :after, :after_meth, if: :condition
562
+ # set_callback :save, :around, ->(r, block) { stuff; result = block.call; stuff }
563
+ #
564
+ # The second argument indicates whether the callback is to be run +:before+,
565
+ # +:after+, or +:around+ the event. If omitted, +:before+ is assumed. This
566
+ # means the first example above can also be written as:
567
+ #
568
+ # set_callback :save, :before_meth
569
+ #
570
+ # The callback can be specified as a symbol naming an instance method; as a
571
+ # proc, lambda, or block; as a string to be instance evaluated; or as an
572
+ # object that responds to a certain method determined by the <tt>:scope</tt>
573
+ # argument to +define_callbacks+.
574
+ #
575
+ # If a proc, lambda, or block is given, its body is evaluated in the context
576
+ # of the current object. It can also optionally accept the current object as
577
+ # an argument.
578
+ #
579
+ # Before and around callbacks are called in the order that they are set;
580
+ # after callbacks are called in the reverse order.
581
+ #
582
+ # Around callbacks can access the return value from the event, if it
583
+ # wasn't halted, from the +yield+ call.
584
+ #
585
+ # ===== Options
586
+ #
587
+ # * <tt>:if</tt> - A symbol, a string or an array of symbols and strings,
588
+ # each naming an instance method or a proc; the callback will be called
589
+ # only when they all return a true value.
590
+ # * <tt>:unless</tt> - A symbol, a string or an array of symbols and
591
+ # strings, each naming an instance method or a proc; the callback will
592
+ # be called only when they all return a false value.
593
+ # * <tt>:prepend</tt> - If +true+, the callback will be prepended to the
594
+ # existing chain rather than appended.
595
+ def set_callback(name, *filter_list, &block)
596
+ type, filters, options = normalize_callback_params(filter_list, block)
597
+ self_chain = get_callbacks name
598
+ mapped = filters.map do |filter|
599
+ Callback.build(self_chain, filter, type, options)
600
+ end
601
+
602
+ __update_callbacks(name) do |target, chain|
603
+ options[:prepend] ? chain.prepend(*mapped) : chain.append(*mapped)
604
+ target.set_callbacks name, chain
605
+ end
606
+ end
607
+
608
+ # Skip a previously set callback. Like +set_callback+, <tt>:if</tt> or
609
+ # <tt>:unless</tt> options may be passed in order to control when the
610
+ # callback is skipped.
611
+ #
612
+ # class Writer < Person
613
+ # skip_callback :validate, :before, :check_membership, if: -> { self.age > 18 }
614
+ # end
615
+ #
616
+ # An <tt>ArgumentError</tt> will be raised if the callback has not
617
+ # already been set (unless the <tt>:raise</tt> option is set to <tt>false</tt>).
618
+ def skip_callback(name, *filter_list, &block)
619
+ type, filters, options = normalize_callback_params(filter_list, block)
620
+ options[:raise] = true unless options.key?(:raise)
621
+
622
+ __update_callbacks(name) do |target, chain|
623
+ filters.each do |filter|
624
+ callback = chain.find {|c| c.matches?(type, filter) }
625
+
626
+ if !callback && options[:raise]
627
+ raise ArgumentError, "#{type.to_s.capitalize} #{name} callback #{filter.inspect} has not been defined"
628
+ end
629
+
630
+ if callback && (options.key?(:if) || options.key?(:unless))
631
+ new_callback = callback.merge_conditional_options(chain, if_option: options[:if], unless_option: options[:unless])
632
+ chain.insert(chain.index(callback), new_callback)
633
+ end
634
+
635
+ chain.delete(callback)
636
+ end
637
+ target.set_callbacks name, chain
638
+ end
639
+ end
640
+
641
+ # Remove all set callbacks for the given event.
642
+ def reset_callbacks(name)
643
+ callbacks = get_callbacks name
644
+ set_callbacks name, callbacks.dup.clear
645
+ end
646
+
647
+ # Define sets of events in the object life cycle that support callbacks.
648
+ #
649
+ # define_callbacks :validate
650
+ # define_callbacks :initialize, :save, :destroy
651
+ #
652
+ # ===== Options
653
+ #
654
+ # * <tt>:terminator</tt> - Determines when a before filter will halt the
655
+ # callback chain, preventing following before and around callbacks from
656
+ # being called and the event from being triggered.
657
+ # This should be a lambda to be executed.
658
+ # The current object and the result lambda of the callback will be provided
659
+ # to the terminator lambda.
660
+ #
661
+ # define_callbacks :validate, terminator: ->(target, result_lambda) { result_lambda.call == false }
662
+ #
663
+ # In this example, if any before validate callbacks returns +false+,
664
+ # any successive before and around callback is not executed.
665
+ #
666
+ # The default terminator halts the chain when a callback throws +:abort+.
667
+ #
668
+ # * <tt>:skip_after_callbacks_if_terminated</tt> - Determines if after
669
+ # callbacks should be terminated by the <tt>:terminator</tt> option. By
670
+ # default after callbacks are executed no matter if callback chain was
671
+ # terminated or not. This option makes sense only when <tt>:terminator</tt>
672
+ # option is specified.
673
+ #
674
+ # * <tt>:scope</tt> - Indicates which methods should be executed when an
675
+ # object is used as a callback.
676
+ #
677
+ # class Audit
678
+ # def before(caller)
679
+ # puts 'Audit: before is called'
680
+ # end
681
+ #
682
+ # def before_save(caller)
683
+ # puts 'Audit: before_save is called'
684
+ # end
685
+ # end
686
+ #
687
+ # class Account
688
+ # include CoreExt::Callbacks
689
+ #
690
+ # define_callbacks :save
691
+ # set_callback :save, :before, Audit.new
692
+ #
693
+ # def save
694
+ # run_callbacks :save do
695
+ # puts 'save in main'
696
+ # end
697
+ # end
698
+ # end
699
+ #
700
+ # In the above case whenever you save an account the method
701
+ # <tt>Audit#before</tt> will be called. On the other hand
702
+ #
703
+ # define_callbacks :save, scope: [:kind, :name]
704
+ #
705
+ # would trigger <tt>Audit#before_save</tt> instead. That's constructed
706
+ # by calling <tt>#{kind}_#{name}</tt> on the given instance. In this
707
+ # case "kind" is "before" and "name" is "save". In this context +:kind+
708
+ # and +:name+ have special meanings: +:kind+ refers to the kind of
709
+ # callback (before/after/around) and +:name+ refers to the method on
710
+ # which callbacks are being defined.
711
+ #
712
+ # A declaration like
713
+ #
714
+ # define_callbacks :save, scope: [:name]
715
+ #
716
+ # would call <tt>Audit#save</tt>.
717
+ #
718
+ # NOTE: +method_name+ passed to `define_model_callbacks` must not end with
719
+ # `!`, `?` or `=`.
720
+ def define_callbacks(*names)
721
+ options = names.extract_options!
722
+
723
+ names.each do |name|
724
+ class_attribute "_#{name}_callbacks"
725
+ set_callbacks name, CallbackChain.new(name, options)
726
+
727
+ module_eval <<-RUBY, __FILE__, __LINE__ + 1
728
+ def _run_#{name}_callbacks(&block)
729
+ __run_callbacks__(_#{name}_callbacks, &block)
730
+ end
731
+ RUBY
732
+ end
733
+ end
734
+
735
+ protected
736
+
737
+ def get_callbacks(name) # :nodoc:
738
+ send "_#{name}_callbacks"
739
+ end
740
+
741
+ def set_callbacks(name, callbacks) # :nodoc:
742
+ send "_#{name}_callbacks=", callbacks
743
+ end
744
+
745
+ def deprecated_false_terminator # :nodoc:
746
+ Proc.new do |target, result_lambda|
747
+ terminate = true
748
+ catch(:abort) do
749
+ result = result_lambda.call if result_lambda.is_a?(Proc)
750
+ if Callbacks.halt_and_display_warning_on_return_false && result == false
751
+ display_deprecation_warning_for_false_terminator
752
+ else
753
+ terminate = false
754
+ end
755
+ end
756
+ terminate
757
+ end
758
+ end
759
+
760
+ private
761
+
762
+ def display_deprecation_warning_for_false_terminator
763
+ CoreExt::Deprecation.warn(<<-MSG.squish)
764
+ Returning `false` in Active Record and Active Model callbacks will not implicitly halt a callback chain in the next release of Rails.
765
+ To explicitly halt the callback chain, please use `throw :abort` instead.
766
+ MSG
767
+ end
768
+ end
769
+ end
770
+ end