couchrest 0.33 → 0.34

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 (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