couchrest 0.33 → 0.34

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. data/README.md +8 -127
  2. data/Rakefile +20 -36
  3. data/THANKS.md +2 -1
  4. data/history.txt +25 -0
  5. data/lib/couchrest.rb +5 -4
  6. data/lib/couchrest/core/database.rb +26 -21
  7. data/lib/couchrest/core/document.rb +4 -3
  8. data/lib/couchrest/helper/streamer.rb +11 -4
  9. data/lib/couchrest/mixins/attribute_protection.rb +74 -0
  10. data/lib/couchrest/mixins/callbacks.rb +187 -138
  11. data/lib/couchrest/mixins/collection.rb +3 -16
  12. data/lib/couchrest/mixins/extended_attachments.rb +1 -1
  13. data/lib/couchrest/mixins/extended_document_mixins.rb +1 -0
  14. data/lib/couchrest/mixins/properties.rb +71 -44
  15. data/lib/couchrest/mixins/validation.rb +18 -29
  16. data/lib/couchrest/more/casted_model.rb +29 -1
  17. data/lib/couchrest/more/extended_document.rb +73 -25
  18. data/lib/couchrest/more/property.rb +20 -1
  19. data/lib/couchrest/support/class.rb +81 -67
  20. data/lib/couchrest/support/rails.rb +12 -5
  21. data/lib/couchrest/validation/auto_validate.rb +5 -9
  22. data/lib/couchrest/validation/validators/confirmation_validator.rb +11 -3
  23. data/lib/couchrest/validation/validators/format_validator.rb +8 -3
  24. data/lib/couchrest/validation/validators/length_validator.rb +10 -5
  25. data/lib/couchrest/validation/validators/numeric_validator.rb +6 -1
  26. data/lib/couchrest/validation/validators/required_field_validator.rb +8 -3
  27. data/spec/couchrest/core/couchrest_spec.rb +48 -2
  28. data/spec/couchrest/core/database_spec.rb +22 -10
  29. data/spec/couchrest/core/document_spec.rb +9 -1
  30. data/spec/couchrest/helpers/streamer_spec.rb +31 -2
  31. data/spec/couchrest/more/attribute_protection_spec.rb +94 -0
  32. data/spec/couchrest/more/casted_extended_doc_spec.rb +2 -4
  33. data/spec/couchrest/more/casted_model_spec.rb +230 -1
  34. data/spec/couchrest/more/extended_doc_attachment_spec.rb +2 -2
  35. data/spec/couchrest/more/extended_doc_spec.rb +173 -15
  36. data/spec/couchrest/more/extended_doc_view_spec.rb +17 -10
  37. data/spec/couchrest/more/property_spec.rb +97 -3
  38. data/spec/fixtures/more/article.rb +4 -3
  39. data/spec/fixtures/more/card.rb +1 -1
  40. data/spec/fixtures/more/cat.rb +5 -3
  41. data/spec/fixtures/more/event.rb +4 -1
  42. data/spec/fixtures/more/invoice.rb +2 -2
  43. data/spec/fixtures/more/person.rb +1 -0
  44. data/spec/fixtures/more/user.rb +22 -0
  45. metadata +46 -13
@@ -0,0 +1,74 @@
1
+ module CouchRest
2
+ module Mixins
3
+ module AttributeProtection
4
+ # Attribute protection from mass assignment to CouchRest properties
5
+ #
6
+ # Protected methods will be removed from
7
+ # * new
8
+ # * update_attributes
9
+ # * upate_attributes_without_saving
10
+ # * attributes=
11
+ #
12
+ # There are two modes of protection
13
+ # 1) Declare accessible poperties, assume all the rest are protected
14
+ # property :name, :accessible => true
15
+ # property :admin # this will be automatically protected
16
+ #
17
+ # 2) Declare protected properties, assume all the rest are accessible
18
+ # property :name # this will not be protected
19
+ # property :admin, :protected => true
20
+ #
21
+ # Note: you cannot set both flags in a single class
22
+
23
+ def self.included(base)
24
+ base.extend(ClassMethods)
25
+ end
26
+
27
+ module ClassMethods
28
+ def accessible_properties
29
+ properties.select { |prop| prop.options[:accessible] }
30
+ end
31
+
32
+ def protected_properties
33
+ properties.select { |prop| prop.options[:protected] }
34
+ end
35
+ end
36
+
37
+ def accessible_properties
38
+ self.class.accessible_properties
39
+ end
40
+
41
+ def protected_properties
42
+ self.class.protected_properties
43
+ end
44
+
45
+ def remove_protected_attributes(attributes)
46
+ protected_names = properties_to_remove_from_mass_assignment.map { |prop| prop.name }
47
+ return attributes if protected_names.empty?
48
+
49
+ attributes.reject! do |property_name, property_value|
50
+ protected_names.include?(property_name.to_s)
51
+ end
52
+
53
+ attributes || {}
54
+ end
55
+
56
+ private
57
+
58
+ def properties_to_remove_from_mass_assignment
59
+ has_protected = !protected_properties.empty?
60
+ has_accessible = !accessible_properties.empty?
61
+
62
+ if !has_protected && !has_accessible
63
+ []
64
+ elsif has_protected && !has_accessible
65
+ protected_properties
66
+ elsif has_accessible && !has_protected
67
+ properties.reject { |prop| prop.options[:accessible] }
68
+ else
69
+ raise "Set either :accessible or :protected for #{self.class}, but not both"
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -1,8 +1,29 @@
1
- require File.join(File.dirname(__FILE__), '..', 'support', 'class')
1
+ # Copyright (c) 2006-2009 David Heinemeier Hansson
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining
4
+ # a copy of this software and associated documentation files (the
5
+ # "Software"), to deal in the Software without restriction, including
6
+ # without limitation the rights to use, copy, modify, merge, publish,
7
+ # distribute, sublicense, and/or sell copies of the Software, and to
8
+ # permit persons to whom the Software is furnished to do so, subject to
9
+ # the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be
12
+ # included in all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+ #
22
+ # Extracted from ActiveSupport::NewCallbacks written by Yehuda Katz
23
+ # http://github.com/rails/rails/raw/d6e4113c83a9d55be6f2af247da2cecaa855f43b/activesupport/lib/active_support/new_callbacks.rb
24
+ # http://github.com/rails/rails/commit/1126a85aed576402d978e6f76eb393b6baaa9541
2
25
 
3
- # Extracted from ActiveSupport::Callbacks written by Yehuda Katz
4
- # http://github.com/wycats/rails/raw/abstract_controller/activesupport/lib/active_support/new_callbacks.rb
5
- # http://github.com/wycats/rails/raw/18b405f154868204a8f332888871041a7bad95e1/activesupport/lib/active_support/callbacks.rb
26
+ require File.join(File.dirname(__FILE__), '..', 'support', 'class')
6
27
 
7
28
  module CouchRest
8
29
  # Callbacks are hooks into the lifecycle of an object that allow you to trigger logic
@@ -85,19 +106,18 @@ module CouchRest
85
106
  def self.included(klass)
86
107
  klass.extend ClassMethods
87
108
  end
88
-
109
+
89
110
  def run_callbacks(kind, options = {}, &blk)
90
111
  send("_run_#{kind}_callbacks", &blk)
91
112
  end
92
-
113
+
93
114
  class Callback
94
115
  @@_callback_sequence = 0
95
-
116
+
96
117
  attr_accessor :filter, :kind, :name, :options, :per_key, :klass
97
- def initialize(filter, kind, options, klass, name)
118
+ def initialize(filter, kind, options, klass)
98
119
  @kind, @klass = kind, klass
99
- @name = name
100
-
120
+
101
121
  normalize_options!(options)
102
122
 
103
123
  @per_key = options.delete(:per_key)
@@ -108,7 +128,7 @@ module CouchRest
108
128
 
109
129
  _compile_per_key_options
110
130
  end
111
-
131
+
112
132
  def clone(klass)
113
133
  obj = super()
114
134
  obj.klass = klass
@@ -120,23 +140,22 @@ module CouchRest
120
140
  obj.options[:unless] = @options[:unless].dup
121
141
  obj
122
142
  end
123
-
143
+
124
144
  def normalize_options!(options)
125
- options[:if] = Array(options[:if])
126
- options[:unless] = Array(options[:unless])
145
+ options[:if] = Array.wrap(options[:if])
146
+ options[:unless] = Array.wrap(options[:unless])
127
147
 
128
148
  options[:per_key] ||= {}
129
- options[:per_key][:if] = Array(options[:per_key][:if])
130
- options[:per_key][:unless] = Array(options[:per_key][:unless])
149
+ options[:per_key][:if] = Array.wrap(options[:per_key][:if])
150
+ options[:per_key][:unless] = Array.wrap(options[:per_key][:unless])
131
151
  end
132
-
152
+
133
153
  def next_id
134
154
  @@_callback_sequence += 1
135
155
  end
136
-
137
- def matches?(_kind, _name, _filter)
156
+
157
+ def matches?(_kind, _filter)
138
158
  @kind == _kind &&
139
- @name == _name &&
140
159
  @filter == _filter
141
160
  end
142
161
 
@@ -144,11 +163,11 @@ module CouchRest
144
163
  filter_options[:if].push(new_options[:unless]) if new_options.key?(:unless)
145
164
  filter_options[:unless].push(new_options[:if]) if new_options.key?(:if)
146
165
  end
147
-
166
+
148
167
  def recompile!(_options, _per_key)
149
168
  _update_filter(self.options, _options)
150
169
  _update_filter(self.per_key, _per_key)
151
-
170
+
152
171
  @callback_id = next_id
153
172
  @filter = _compile_filter(@raw_filter)
154
173
  @compiled_options = _compile_options(@options)
@@ -164,19 +183,19 @@ module CouchRest
164
183
  end
165
184
  RUBY_EVAL
166
185
  end
167
-
186
+
168
187
  # This will supply contents for before and around filters, and no
169
188
  # contents for after filters (for the forward pass).
170
189
  def start(key = nil, options = {})
171
190
  object, terminator = (options || {}).values_at(:object, :terminator)
172
-
191
+
173
192
  return if key && !object.send("_one_time_conditions_valid_#{@callback_id}?")
174
-
193
+
175
194
  terminator ||= false
176
-
195
+
177
196
  # options[0] is the compiled form of supplied conditions
178
197
  # options[1] is the "end" for the conditional
179
-
198
+
180
199
  if @kind == :before || @kind == :around
181
200
  if @kind == :before
182
201
  # if condition # before_save :filter_name, :if => :condition
@@ -185,9 +204,10 @@ module CouchRest
185
204
  filter = <<-RUBY_EVAL
186
205
  unless halted
187
206
  result = #{@filter}
188
- halted ||= (#{terminator})
207
+ halted = (#{terminator})
189
208
  end
190
209
  RUBY_EVAL
210
+
191
211
  [@compiled_options[0], filter, @compiled_options[1]].compact.join("\n")
192
212
  else
193
213
  # Compile around filters with conditions into proxy methods
@@ -204,9 +224,9 @@ module CouchRest
204
224
  # yield self
205
225
  # end
206
226
  # end
207
-
227
+
208
228
  name = "_conditional_callback_#{@kind}_#{next_id}"
209
- txt = <<-RUBY_EVAL
229
+ txt, line = <<-RUBY_EVAL, __LINE__ + 1
210
230
  def #{name}(halted)
211
231
  #{@compiled_options[0] || "if true"} && !halted
212
232
  #{@filter} do
@@ -217,19 +237,19 @@ module CouchRest
217
237
  end
218
238
  end
219
239
  RUBY_EVAL
220
- @klass.class_eval(txt)
240
+ @klass.class_eval(txt, __FILE__, line)
221
241
  "#{name}(halted) do"
222
242
  end
223
243
  end
224
244
  end
225
-
245
+
226
246
  # This will supply contents for around and after filters, but not
227
247
  # before filters (for the backward pass).
228
248
  def end(key = nil, options = {})
229
249
  object = (options || {})[:object]
230
-
250
+
231
251
  return if key && !object.send("_one_time_conditions_valid_#{@callback_id}?")
232
-
252
+
233
253
  if @kind == :around || @kind == :after
234
254
  # if condition # after_save :filter_name, :if => :condition
235
255
  # filter_name
@@ -241,27 +261,27 @@ module CouchRest
241
261
  end
242
262
  end
243
263
  end
244
-
264
+
245
265
  private
246
266
  # Options support the same options as filters themselves (and support
247
267
  # symbols, string, procs, and objects), so compile a conditional
248
268
  # expression based on the options
249
269
  def _compile_options(options)
250
270
  return [] if options[:if].empty? && options[:unless].empty?
251
-
271
+
252
272
  conditions = []
253
-
273
+
254
274
  unless options[:if].empty?
255
- conditions << Array(_compile_filter(options[:if]))
275
+ conditions << Array.wrap(_compile_filter(options[:if]))
256
276
  end
257
-
277
+
258
278
  unless options[:unless].empty?
259
- conditions << Array(_compile_filter(options[:unless])).map {|f| "!#{f}"}
279
+ conditions << Array.wrap(_compile_filter(options[:unless])).map {|f| "!#{f}"}
260
280
  end
261
-
281
+
262
282
  ["if #{conditions.flatten.join(" && ")}", "end"]
263
283
  end
264
-
284
+
265
285
  # Filters support:
266
286
  # Arrays:: Used in conditions. This is used to specify
267
287
  # multiple conditions. Used internally to
@@ -287,63 +307,72 @@ module CouchRest
287
307
  filter.map {|f| _compile_filter(f)}
288
308
  when Symbol
289
309
  filter
310
+ when String
311
+ "(#{filter})"
290
312
  when Proc
291
313
  @klass.send(:define_method, method_name, &filter)
292
- method_name << (filter.arity == 1 ? "(self)" : "")
293
- when String
294
- @klass.class_eval <<-RUBY_EVAL
295
- def #{method_name}
296
- #{filter}
314
+ return method_name if filter.arity == 0
315
+
316
+ method_name << (filter.arity == 1 ? "(self)" : " self, Proc.new ")
317
+ else
318
+ @klass.send(:define_method, "#{method_name}_object") { filter }
319
+
320
+ _normalize_legacy_filter(kind, filter)
321
+
322
+ @klass.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
323
+ def #{method_name}(&blk)
324
+ #{method_name}_object.send(:#{kind}, self, &blk)
297
325
  end
298
326
  RUBY_EVAL
299
- method_name
300
- else
301
- kind, name = @kind, @name
302
- @klass.send(:define_method, method_name) do
303
- filter.send("#{kind}_#{name}", self)
304
- end
327
+
305
328
  method_name
306
329
  end
307
330
  end
308
- end
309
331
 
310
- # This method_missing is supplied to catch callbacks with keys and create
311
- # the appropriate callback for future use.
312
- def method_missing(meth, *args, &blk)
313
- if meth.to_s =~ /_run__([\w:]+)__(\w+)__(\w+)__callbacks/
314
- return self.class._create_and_run_keyed_callback($1, $2.to_sym, $3.to_sym, self, &blk)
332
+ def _normalize_legacy_filter(kind, filter)
333
+ if !filter.respond_to?(kind) && filter.respond_to?(:filter)
334
+ filter.class_eval(
335
+ "def #{kind}(context, &block) filter(context, &block) end",
336
+ __FILE__, __LINE__ - 1)
337
+ elsif filter.respond_to?(:before) && filter.respond_to?(:after) && kind == :around
338
+ def filter.around(context)
339
+ should_continue = before(context)
340
+ yield if should_continue
341
+ after(context)
342
+ end
343
+ end
315
344
  end
316
- super
345
+
317
346
  end
318
-
347
+
319
348
  # An Array with a compile method
320
349
  class CallbackChain < Array
321
350
  def initialize(symbol)
322
351
  @symbol = symbol
323
352
  end
324
-
353
+
325
354
  def compile(key = nil, options = {})
326
355
  method = []
327
356
  method << "halted = false"
328
357
  each do |callback|
329
358
  method << callback.start(key, options)
330
359
  end
331
- method << "yield self if block_given?"
360
+ method << "yield self if block_given? && !halted"
332
361
  reverse_each do |callback|
333
362
  method << callback.end(key, options)
334
363
  end
335
364
  method.compact.join("\n")
336
365
  end
337
-
366
+
338
367
  def clone(klass)
339
368
  chain = CallbackChain.new(@symbol)
340
369
  chain.push(*map {|c| c.clone(klass)})
341
370
  end
342
371
  end
343
-
372
+
344
373
  module ClassMethods
345
- CHAINS = {:before => :before, :around => :before, :after => :after} unless self.const_defined?("CHAINS")
346
-
374
+ #CHAINS = {:before => :before, :around => :before, :after => :after}
375
+
347
376
  # Make the _run_save_callbacks method. The generated method takes
348
377
  # a block that it'll yield to. It'll call the before and around filters
349
378
  # in order, yield the block, and then run the after filters.
@@ -355,43 +384,45 @@ module CouchRest
355
384
  # The _run_save_callbacks method can optionally take a key, which
356
385
  # will be used to compile an optimized callback method for each
357
386
  # key. See #define_callbacks for more information.
358
- def _define_runner(symbol, str, options)
359
- str = <<-RUBY_EVAL
360
- def _run_#{symbol}_callbacks(key = nil)
387
+ def _define_runner(symbol)
388
+ body = send("_#{symbol}_callback").
389
+ compile(nil, :terminator => send("_#{symbol}_terminator"))
390
+
391
+ body, line = <<-RUBY_EVAL, __LINE__ + 1
392
+ def _run_#{symbol}_callbacks(key = nil, &blk)
361
393
  if key
362
- send("_run__\#{self.class.name.split("::").last}__#{symbol}__\#{key}__callbacks") { yield if block_given? }
394
+ name = "_run__\#{self.class.name.hash.abs}__#{symbol}__\#{key.hash.abs}__callbacks"
395
+
396
+ unless respond_to?(name)
397
+ self.class._create_keyed_callback(name, :#{symbol}, self, &blk)
398
+ end
399
+
400
+ send(name, &blk)
363
401
  else
364
- #{str}
402
+ #{body}
365
403
  end
366
404
  end
367
405
  RUBY_EVAL
368
-
369
- class_eval str, __FILE__, __LINE__ + 1
370
-
371
- before_name, around_name, after_name =
372
- options.values_at(:before, :after, :around)
406
+
407
+ undef_method "_run_#{symbol}_callbacks" if method_defined?("_run_#{symbol}_callbacks")
408
+ class_eval body, __FILE__, line
373
409
  end
374
-
410
+
375
411
  # This is called the first time a callback is called with a particular
376
412
  # key. It creates a new callback method for the key, calculating
377
413
  # which callbacks can be omitted because of per_key conditions.
378
- def _create_and_run_keyed_callback(klass, kind, key, obj, &blk)
414
+ def _create_keyed_callback(name, kind, obj, &blk)
379
415
  @_keyed_callbacks ||= {}
380
- @_keyed_callbacks[[kind, key]] ||= begin
381
- str = self.send("_#{kind}_callbacks").compile(key, :object => obj, :terminator => self.send("_#{kind}_terminator"))
416
+ @_keyed_callbacks[name] ||= begin
417
+ str = send("_#{kind}_callback").
418
+ compile(name, :object => obj, :terminator => send("_#{kind}_terminator"))
419
+
420
+ class_eval "def #{name}() #{str} end", __FILE__, __LINE__
382
421
 
383
- self.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
384
- def _run__#{klass.split("::").last}__#{kind}__#{key}__callbacks
385
- #{str}
386
- end
387
- RUBY_EVAL
388
-
389
422
  true
390
423
  end
391
-
392
- obj.send("_run__#{klass.split("::").last}__#{kind}__#{key}__callbacks", &blk)
393
424
  end
394
-
425
+
395
426
  # Define callbacks.
396
427
  #
397
428
  # Creates a <name>_callback method that you can use to add callbacks.
@@ -423,59 +454,77 @@ module CouchRest
423
454
  # In that case, each action_name would get its own compiled callback
424
455
  # method that took into consideration the per_key conditions. This
425
456
  # is a speed improvement for ActionPack.
457
+ def _update_callbacks(name, filters = CallbackChain.new(name), block = nil)
458
+ type = [:before, :after, :around].include?(filters.first) ? filters.shift : :before
459
+ options = filters.last.is_a?(Hash) ? filters.pop : {}
460
+ filters.unshift(block) if block
461
+
462
+ callbacks = send("_#{name}_callback")
463
+ yield callbacks, type, filters, options if block_given?
464
+
465
+ _define_runner(name)
466
+ end
467
+
468
+ alias_method :_reset_callbacks, :_update_callbacks
469
+
470
+ def set_callback(name, *filters, &block)
471
+ _update_callbacks(name, filters, block) do |callbacks, type, filters, options|
472
+ filters.map! do |filter|
473
+ # overrides parent class
474
+ callbacks.delete_if {|c| c.matches?(type, filter) }
475
+ Callback.new(filter, type, options.dup, self)
476
+ end
477
+
478
+ options[:prepend] ? callbacks.unshift(*filters) : callbacks.push(*filters)
479
+ end
480
+ end
481
+
482
+ def skip_callback(name, *filters, &block)
483
+ _update_callbacks(name, filters, block) do |callbacks, type, filters, options|
484
+ filters.each do |filter|
485
+ callbacks = send("_#{name}_callback=", callbacks.clone(self))
486
+
487
+ filter = callbacks.find {|c| c.matches?(type, filter) }
488
+
489
+ if filter && options.any?
490
+ filter.recompile!(options, options[:per_key] || {})
491
+ else
492
+ callbacks.delete(filter)
493
+ end
494
+ end
495
+ end
496
+ end
497
+
426
498
  def define_callbacks(*symbols)
427
499
  terminator = symbols.pop if symbols.last.is_a?(String)
428
500
  symbols.each do |symbol|
429
- self.extlib_inheritable_accessor("_#{symbol}_terminator")
430
- self.send("_#{symbol}_terminator=", terminator)
431
- self.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
432
- extlib_inheritable_accessor :_#{symbol}_callbacks
433
- self._#{symbol}_callbacks = CallbackChain.new(:#{symbol})
434
-
435
- def self.#{symbol}_callback(*filters, &blk)
436
- type = [:before, :after, :around].include?(filters.first) ? filters.shift : :before
437
- options = filters.last.is_a?(Hash) ? filters.pop : {}
438
- filters.unshift(blk) if block_given?
439
-
440
- filters.map! do |filter|
441
- # overrides parent class
442
- self._#{symbol}_callbacks.delete_if {|c| c.matches?(type, :#{symbol}, filter)}
443
- Callback.new(filter, type, options.dup, self, :#{symbol})
444
- end
445
- self._#{symbol}_callbacks.push(*filters)
446
- _define_runner(:#{symbol},
447
- self._#{symbol}_callbacks.compile(nil, :terminator => _#{symbol}_terminator),
448
- options)
449
- end
450
-
451
- def self.skip_#{symbol}_callback(*filters, &blk)
452
- type = [:before, :after, :around].include?(filters.first) ? filters.shift : :before
453
- options = filters.last.is_a?(Hash) ? filters.pop : {}
454
- filters.unshift(blk) if block_given?
455
- filters.each do |filter|
456
- self._#{symbol}_callbacks = self._#{symbol}_callbacks.clone(self)
457
-
458
- filter = self._#{symbol}_callbacks.find {|c| c.matches?(type, :#{symbol}, filter) }
459
- per_key = options[:per_key] || {}
460
- if filter
461
- filter.recompile!(options, per_key)
462
- else
463
- self._#{symbol}_callbacks.delete(filter)
501
+ extlib_inheritable_accessor("_#{symbol}_terminator") { terminator }
502
+
503
+ extlib_inheritable_accessor("_#{symbol}_callback") do
504
+ CallbackChain.new(symbol)
505
+ end
506
+
507
+ _define_runner(symbol)
508
+
509
+ # Define more convenient callback methods
510
+ # set_callback(:save, :before) becomes before_save
511
+ [:before, :after, :around].each do |filter|
512
+ self.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
513
+ def self.#{filter}_#{symbol}(*symbols, &blk)
514
+ _alias_callbacks(symbols, blk) do |callback, options|
515
+ set_callback(:#{symbol}, :#{filter}, callback, options)
464
516
  end
465
- _define_runner(:#{symbol},
466
- self._#{symbol}_callbacks.compile(nil, :terminator => _#{symbol}_terminator),
467
- options)
468
517
  end
469
-
470
- end
471
-
472
- def self.reset_#{symbol}_callbacks
473
- self._#{symbol}_callbacks = CallbackChain.new(:#{symbol})
474
- _define_runner(:#{symbol}, self._#{symbol}_callbacks.compile, {})
475
- end
476
-
477
- self.#{symbol}_callback(:before)
478
- RUBY_EVAL
518
+ RUBY_EVAL
519
+ end
520
+ end
521
+ end
522
+
523
+ def _alias_callbacks(callbacks, block)
524
+ options = callbacks.last.is_a?(Hash) ? callbacks.pop : {}
525
+ callbacks.push(block) if block
526
+ callbacks.each do |callback|
527
+ yield callback, options
479
528
  end
480
529
  end
481
530
  end