davber_couchrest_extended_document 1.0.0

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 (71) hide show
  1. data/LICENSE +176 -0
  2. data/README.md +250 -0
  3. data/Rakefile +69 -0
  4. data/THANKS.md +22 -0
  5. data/examples/model/example.rb +144 -0
  6. data/history.txt +165 -0
  7. data/lib/couchrest/casted_array.rb +39 -0
  8. data/lib/couchrest/casted_model.rb +53 -0
  9. data/lib/couchrest/extended_document.rb +262 -0
  10. data/lib/couchrest/mixins.rb +12 -0
  11. data/lib/couchrest/mixins/attribute_protection.rb +74 -0
  12. data/lib/couchrest/mixins/attributes.rb +75 -0
  13. data/lib/couchrest/mixins/callbacks.rb +534 -0
  14. data/lib/couchrest/mixins/class_proxy.rb +120 -0
  15. data/lib/couchrest/mixins/collection.rb +260 -0
  16. data/lib/couchrest/mixins/design_doc.rb +159 -0
  17. data/lib/couchrest/mixins/document_queries.rb +82 -0
  18. data/lib/couchrest/mixins/extended_attachments.rb +73 -0
  19. data/lib/couchrest/mixins/properties.rb +130 -0
  20. data/lib/couchrest/mixins/typecast.rb +174 -0
  21. data/lib/couchrest/mixins/views.rb +148 -0
  22. data/lib/couchrest/property.rb +96 -0
  23. data/lib/couchrest/support/couchrest.rb +19 -0
  24. data/lib/couchrest/support/rails.rb +42 -0
  25. data/lib/couchrest/validation.rb +246 -0
  26. data/lib/couchrest/validation/auto_validate.rb +156 -0
  27. data/lib/couchrest/validation/contextual_validators.rb +78 -0
  28. data/lib/couchrest/validation/validation_errors.rb +125 -0
  29. data/lib/couchrest/validation/validators/absent_field_validator.rb +74 -0
  30. data/lib/couchrest/validation/validators/confirmation_validator.rb +107 -0
  31. data/lib/couchrest/validation/validators/format_validator.rb +122 -0
  32. data/lib/couchrest/validation/validators/formats/email.rb +66 -0
  33. data/lib/couchrest/validation/validators/formats/url.rb +43 -0
  34. data/lib/couchrest/validation/validators/generic_validator.rb +120 -0
  35. data/lib/couchrest/validation/validators/length_validator.rb +139 -0
  36. data/lib/couchrest/validation/validators/method_validator.rb +89 -0
  37. data/lib/couchrest/validation/validators/numeric_validator.rb +109 -0
  38. data/lib/couchrest/validation/validators/required_field_validator.rb +114 -0
  39. data/lib/couchrest_extended_document.rb +23 -0
  40. data/spec/couchrest/attribute_protection_spec.rb +150 -0
  41. data/spec/couchrest/casted_extended_doc_spec.rb +79 -0
  42. data/spec/couchrest/casted_model_spec.rb +424 -0
  43. data/spec/couchrest/extended_doc_attachment_spec.rb +148 -0
  44. data/spec/couchrest/extended_doc_inherited_spec.rb +40 -0
  45. data/spec/couchrest/extended_doc_spec.rb +869 -0
  46. data/spec/couchrest/extended_doc_subclass_spec.rb +101 -0
  47. data/spec/couchrest/extended_doc_view_spec.rb +529 -0
  48. data/spec/couchrest/property_spec.rb +790 -0
  49. data/spec/fixtures/attachments/README +3 -0
  50. data/spec/fixtures/attachments/couchdb.png +0 -0
  51. data/spec/fixtures/attachments/test.html +11 -0
  52. data/spec/fixtures/more/article.rb +35 -0
  53. data/spec/fixtures/more/card.rb +22 -0
  54. data/spec/fixtures/more/cat.rb +22 -0
  55. data/spec/fixtures/more/course.rb +25 -0
  56. data/spec/fixtures/more/event.rb +8 -0
  57. data/spec/fixtures/more/invoice.rb +17 -0
  58. data/spec/fixtures/more/person.rb +9 -0
  59. data/spec/fixtures/more/question.rb +7 -0
  60. data/spec/fixtures/more/service.rb +12 -0
  61. data/spec/fixtures/more/user.rb +22 -0
  62. data/spec/fixtures/views/lib.js +3 -0
  63. data/spec/fixtures/views/test_view/lib.js +3 -0
  64. data/spec/fixtures/views/test_view/only-map.js +4 -0
  65. data/spec/fixtures/views/test_view/test-map.js +3 -0
  66. data/spec/fixtures/views/test_view/test-reduce.js +3 -0
  67. data/spec/spec.opts +5 -0
  68. data/spec/spec_helper.rb +49 -0
  69. data/utils/remap.rb +27 -0
  70. data/utils/subset.rb +30 -0
  71. metadata +225 -0
@@ -0,0 +1,12 @@
1
+ mixins_dir = File.join(File.dirname(__FILE__), 'mixins')
2
+
3
+ require File.join(mixins_dir, 'callbacks')
4
+ require File.join(mixins_dir, 'properties')
5
+ require File.join(mixins_dir, 'document_queries')
6
+ require File.join(mixins_dir, 'views')
7
+ require File.join(mixins_dir, 'design_doc')
8
+ require File.join(mixins_dir, 'extended_attachments')
9
+ require File.join(mixins_dir, 'class_proxy')
10
+ require File.join(mixins_dir, 'collection')
11
+ require File.join(mixins_dir, 'attribute_protection')
12
+ require File.join(mixins_dir, 'attributes')
@@ -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
@@ -0,0 +1,75 @@
1
+ module CouchRest
2
+ module Mixins
3
+ module Attributes
4
+
5
+ ## Support for handling attributes
6
+ #
7
+ # This would be better in the properties file, but due to scoping issues
8
+ # this is not yet possible.
9
+ #
10
+
11
+ def prepare_all_attributes(doc = {}, options = {})
12
+ apply_all_property_defaults
13
+ if options[:directly_set_attributes]
14
+ directly_set_read_only_attributes(doc)
15
+ else
16
+ remove_protected_attributes(doc)
17
+ end
18
+ directly_set_attributes(doc) unless doc.nil?
19
+ end
20
+
21
+ # Takes a hash as argument, and applies the values by using writer methods
22
+ # for each key. It doesn't save the document at the end. Raises a NoMethodError if the corresponding methods are
23
+ # missing. In case of error, no attributes are changed.
24
+ def update_attributes_without_saving(hash)
25
+ # Remove any protected and update all the rest. Any attributes
26
+ # which do not have a property will simply be ignored.
27
+ attrs = remove_protected_attributes(hash)
28
+ directly_set_attributes(attrs)
29
+ end
30
+ alias :attributes= :update_attributes_without_saving
31
+
32
+ # Takes a hash as argument, and applies the values by using writer methods
33
+ # for each key. Raises a NoMethodError if the corresponding methods are
34
+ # missing. In case of error, no attributes are changed.
35
+ def update_attributes(hash)
36
+ update_attributes_without_saving hash
37
+ save
38
+ end
39
+
40
+ private
41
+
42
+ def directly_set_attributes(hash)
43
+ hash.each do |attribute_name, attribute_value|
44
+ if self.respond_to?("#{attribute_name}=")
45
+ self.send("#{attribute_name}=", hash.delete(attribute_name))
46
+ end
47
+ end
48
+ end
49
+
50
+ def directly_set_read_only_attributes(hash)
51
+ property_list = self.properties.map{|p| p.name}
52
+ hash.each do |attribute_name, attribute_value|
53
+ next if self.respond_to?("#{attribute_name}=")
54
+ if property_list.include?(attribute_name)
55
+ write_attribute(attribute_name, hash.delete(attribute_name))
56
+ end
57
+ end
58
+ end
59
+
60
+ def set_attributes(hash)
61
+ attrs = remove_protected_attributes(hash)
62
+ directly_set_attributes(attrs)
63
+ end
64
+
65
+ def check_properties_exist(attrs)
66
+ property_list = self.properties.map{|p| p.name}
67
+ attrs.each do |attribute_name, attribute_value|
68
+ raise NoMethodError, "Property #{attribute_name} not created" unless respond_to?("#{attribute_name}=") or property_list.include?(attribute_name)
69
+ end
70
+ end
71
+
72
+ end
73
+ end
74
+ end
75
+
@@ -0,0 +1,534 @@
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
25
+
26
+ module CouchRest
27
+ module Mixins
28
+ # Callbacks are hooks into the lifecycle of an object that allow you to trigger logic
29
+ # before or after an alteration of the object state.
30
+ #
31
+ # Mixing in this module allows you to define callbacks in your class.
32
+ #
33
+ # Example:
34
+ # class Storage
35
+ # include ActiveSupport::Callbacks
36
+ #
37
+ # define_callbacks :save
38
+ # end
39
+ #
40
+ # class ConfigStorage < Storage
41
+ # save_callback :before, :saving_message
42
+ # def saving_message
43
+ # puts "saving..."
44
+ # end
45
+ #
46
+ # save_callback :after do |object|
47
+ # puts "saved"
48
+ # end
49
+ #
50
+ # def save
51
+ # _run_save_callbacks do
52
+ # puts "- save"
53
+ # end
54
+ # end
55
+ # end
56
+ #
57
+ # config = ConfigStorage.new
58
+ # config.save
59
+ #
60
+ # Output:
61
+ # saving...
62
+ # - save
63
+ # saved
64
+ #
65
+ # Callbacks from parent classes are inherited.
66
+ #
67
+ # Example:
68
+ # class Storage
69
+ # include ActiveSupport::Callbacks
70
+ #
71
+ # define_callbacks :save
72
+ #
73
+ # save_callback :before, :prepare
74
+ # def prepare
75
+ # puts "preparing save"
76
+ # end
77
+ # end
78
+ #
79
+ # class ConfigStorage < Storage
80
+ # save_callback :before, :saving_message
81
+ # def saving_message
82
+ # puts "saving..."
83
+ # end
84
+ #
85
+ # save_callback :after do |object|
86
+ # puts "saved"
87
+ # end
88
+ #
89
+ # def save
90
+ # _run_save_callbacks do
91
+ # puts "- save"
92
+ # end
93
+ # end
94
+ # end
95
+ #
96
+ # config = ConfigStorage.new
97
+ # config.save
98
+ #
99
+ # Output:
100
+ # preparing save
101
+ # saving...
102
+ # - save
103
+ # saved
104
+ module Callbacks
105
+ def self.included(klass)
106
+ klass.extend ClassMethods
107
+ end
108
+
109
+ def run_callbacks(kind, options = {}, &blk)
110
+ send("_run_#{kind}_callbacks", &blk)
111
+ end
112
+
113
+ class Callback
114
+ @@_callback_sequence = 0
115
+
116
+ attr_accessor :filter, :kind, :name, :options, :per_key, :klass
117
+ def initialize(filter, kind, options, klass)
118
+ @kind, @klass = kind, klass
119
+
120
+ normalize_options!(options)
121
+
122
+ @per_key = options.delete(:per_key)
123
+ @raw_filter, @options = filter, options
124
+ @filter = _compile_filter(filter)
125
+ @compiled_options = _compile_options(options)
126
+ @callback_id = next_id
127
+
128
+ _compile_per_key_options
129
+ end
130
+
131
+ def clone(klass)
132
+ obj = super()
133
+ obj.klass = klass
134
+ obj.per_key = @per_key.dup
135
+ obj.options = @options.dup
136
+ obj.per_key[:if] = @per_key[:if].dup
137
+ obj.per_key[:unless] = @per_key[:unless].dup
138
+ obj.options[:if] = @options[:if].dup
139
+ obj.options[:unless] = @options[:unless].dup
140
+ obj
141
+ end
142
+
143
+ def normalize_options!(options)
144
+ options[:if] = Array.wrap(options[:if])
145
+ options[:unless] = Array.wrap(options[:unless])
146
+
147
+ options[:per_key] ||= {}
148
+ options[:per_key][:if] = Array.wrap(options[:per_key][:if])
149
+ options[:per_key][:unless] = Array.wrap(options[:per_key][:unless])
150
+ end
151
+
152
+ def next_id
153
+ @@_callback_sequence += 1
154
+ end
155
+
156
+ def matches?(_kind, _filter)
157
+ @kind == _kind &&
158
+ @filter == _filter
159
+ end
160
+
161
+ def _update_filter(filter_options, new_options)
162
+ filter_options[:if].push(new_options[:unless]) if new_options.key?(:unless)
163
+ filter_options[:unless].push(new_options[:if]) if new_options.key?(:if)
164
+ end
165
+
166
+ def recompile!(_options, _per_key)
167
+ _update_filter(self.options, _options)
168
+ _update_filter(self.per_key, _per_key)
169
+
170
+ @callback_id = next_id
171
+ @filter = _compile_filter(@raw_filter)
172
+ @compiled_options = _compile_options(@options)
173
+ _compile_per_key_options
174
+ end
175
+
176
+ def _compile_per_key_options
177
+ key_options = _compile_options(@per_key)
178
+
179
+ @klass.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
180
+ def _one_time_conditions_valid_#{@callback_id}?
181
+ true #{key_options[0]}
182
+ end
183
+ RUBY_EVAL
184
+ end
185
+
186
+ # This will supply contents for before and around filters, and no
187
+ # contents for after filters (for the forward pass).
188
+ def start(key = nil, options = {})
189
+ object, terminator = (options || {}).values_at(:object, :terminator)
190
+
191
+ return if key && !object.send("_one_time_conditions_valid_#{@callback_id}?")
192
+
193
+ terminator ||= false
194
+
195
+ # options[0] is the compiled form of supplied conditions
196
+ # options[1] is the "end" for the conditional
197
+
198
+ if @kind == :before || @kind == :around
199
+ if @kind == :before
200
+ # if condition # before_save :filter_name, :if => :condition
201
+ # filter_name
202
+ # end
203
+ filter = <<-RUBY_EVAL
204
+ unless halted
205
+ result = #{@filter}
206
+ halted = (#{terminator})
207
+ end
208
+ RUBY_EVAL
209
+
210
+ [@compiled_options[0], filter, @compiled_options[1]].compact.join("\n")
211
+ else
212
+ # Compile around filters with conditions into proxy methods
213
+ # that contain the conditions.
214
+ #
215
+ # For `around_save :filter_name, :if => :condition':
216
+ #
217
+ # def _conditional_callback_save_17
218
+ # if condition
219
+ # filter_name do
220
+ # yield self
221
+ # end
222
+ # else
223
+ # yield self
224
+ # end
225
+ # end
226
+
227
+ name = "_conditional_callback_#{@kind}_#{next_id}"
228
+ txt, line = <<-RUBY_EVAL, __LINE__ + 1
229
+ def #{name}(halted)
230
+ #{@compiled_options[0] || "if true"} && !halted
231
+ #{@filter} do
232
+ yield self
233
+ end
234
+ else
235
+ yield self
236
+ end
237
+ end
238
+ RUBY_EVAL
239
+ @klass.class_eval(txt, __FILE__, line)
240
+ "#{name}(halted) do"
241
+ end
242
+ end
243
+ end
244
+
245
+ # This will supply contents for around and after filters, but not
246
+ # before filters (for the backward pass).
247
+ def end(key = nil, options = {})
248
+ object = (options || {})[:object]
249
+
250
+ return if key && !object.send("_one_time_conditions_valid_#{@callback_id}?")
251
+
252
+ if @kind == :around || @kind == :after
253
+ # if condition # after_save :filter_name, :if => :condition
254
+ # filter_name
255
+ # end
256
+ if @kind == :after
257
+ [@compiled_options[0], @filter, @compiled_options[1]].compact.join("\n")
258
+ else
259
+ "end"
260
+ end
261
+ end
262
+ end
263
+
264
+ private
265
+ # Options support the same options as filters themselves (and support
266
+ # symbols, string, procs, and objects), so compile a conditional
267
+ # expression based on the options
268
+ def _compile_options(options)
269
+ return [] if options[:if].empty? && options[:unless].empty?
270
+
271
+ conditions = []
272
+
273
+ unless options[:if].empty?
274
+ conditions << Array.wrap(_compile_filter(options[:if]))
275
+ end
276
+
277
+ unless options[:unless].empty?
278
+ conditions << Array.wrap(_compile_filter(options[:unless])).map {|f| "!#{f}"}
279
+ end
280
+
281
+ ["if #{conditions.flatten.join(" && ")}", "end"]
282
+ end
283
+
284
+ # Filters support:
285
+ # Arrays:: Used in conditions. This is used to specify
286
+ # multiple conditions. Used internally to
287
+ # merge conditions from skip_* filters
288
+ # Symbols:: A method to call
289
+ # Strings:: Some content to evaluate
290
+ # Procs:: A proc to call with the object
291
+ # Objects:: An object with a before_foo method on it to call
292
+ #
293
+ # All of these objects are compiled into methods and handled
294
+ # the same after this point:
295
+ # Arrays:: Merged together into a single filter
296
+ # Symbols:: Already methods
297
+ # Strings:: class_eval'ed into methods
298
+ # Procs:: define_method'ed into methods
299
+ # Objects::
300
+ # a method is created that calls the before_foo method
301
+ # on the object.
302
+ def _compile_filter(filter)
303
+ method_name = "_callback_#{@kind}_#{next_id}"
304
+ case filter
305
+ when Array
306
+ filter.map {|f| _compile_filter(f)}
307
+ when Symbol
308
+ filter
309
+ when String
310
+ "(#{filter})"
311
+ when Proc
312
+ @klass.send(:define_method, method_name, &filter)
313
+ return method_name if filter.arity == 0
314
+
315
+ method_name << (filter.arity == 1 ? "(self)" : " self, Proc.new ")
316
+ else
317
+ @klass.send(:define_method, "#{method_name}_object") { filter }
318
+
319
+ _normalize_legacy_filter(kind, filter)
320
+
321
+ @klass.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
322
+ def #{method_name}(&blk)
323
+ #{method_name}_object.send(:#{kind}, self, &blk)
324
+ end
325
+ RUBY_EVAL
326
+
327
+ method_name
328
+ end
329
+ end
330
+
331
+ def _normalize_legacy_filter(kind, filter)
332
+ if !filter.respond_to?(kind) && filter.respond_to?(:filter)
333
+ filter.class_eval(
334
+ "def #{kind}(context, &block) filter(context, &block) end",
335
+ __FILE__, __LINE__ - 1)
336
+ elsif filter.respond_to?(:before) && filter.respond_to?(:after) && kind == :around
337
+ def filter.around(context)
338
+ should_continue = before(context)
339
+ yield if should_continue
340
+ after(context)
341
+ end
342
+ end
343
+ end
344
+
345
+ end
346
+
347
+ # An Array with a compile method
348
+ class CallbackChain < Array
349
+ def initialize(symbol)
350
+ @symbol = symbol
351
+ end
352
+
353
+ def compile(key = nil, options = {})
354
+ method = []
355
+ method << "halted = false"
356
+ each do |callback|
357
+ method << callback.start(key, options)
358
+ end
359
+ method << "yield self if block_given? && !halted"
360
+ reverse_each do |callback|
361
+ method << callback.end(key, options)
362
+ end
363
+ method.compact.join("\n")
364
+ end
365
+
366
+ def clone(klass)
367
+ chain = CallbackChain.new(@symbol)
368
+ chain.push(*map {|c| c.clone(klass)})
369
+ end
370
+ end
371
+
372
+ module ClassMethods
373
+ extend CouchRest::InheritableAttributes
374
+
375
+ #CHAINS = {:before => :before, :around => :before, :after => :after}
376
+
377
+ # Make the _run_save_callbacks method. The generated method takes
378
+ # a block that it'll yield to. It'll call the before and around filters
379
+ # in order, yield the block, and then run the after filters.
380
+ #
381
+ # _run_save_callbacks do
382
+ # save
383
+ # end
384
+ #
385
+ # The _run_save_callbacks method can optionally take a key, which
386
+ # will be used to compile an optimized callback method for each
387
+ # key. See #define_callbacks for more information.
388
+ def _define_runner(symbol)
389
+ body = send("_#{symbol}_callback").
390
+ compile(nil, :terminator => send("_#{symbol}_terminator"))
391
+
392
+ body, line = <<-RUBY_EVAL, __LINE__ + 1
393
+ def _run_#{symbol}_callbacks(key = nil, &blk)
394
+ if key
395
+ name = "_run__\#{self.class.name.hash.abs}__#{symbol}__\#{key.hash.abs}__callbacks"
396
+
397
+ unless respond_to?(name)
398
+ self.class._create_keyed_callback(name, :#{symbol}, self, &blk)
399
+ end
400
+
401
+ send(name, &blk)
402
+ else
403
+ #{body}
404
+ end
405
+ end
406
+ RUBY_EVAL
407
+
408
+ undef_method "_run_#{symbol}_callbacks" if method_defined?("_run_#{symbol}_callbacks")
409
+ class_eval body, __FILE__, line
410
+ end
411
+
412
+ # This is called the first time a callback is called with a particular
413
+ # key. It creates a new callback method for the key, calculating
414
+ # which callbacks can be omitted because of per_key conditions.
415
+ def _create_keyed_callback(name, kind, obj, &blk)
416
+ @_keyed_callbacks ||= {}
417
+ @_keyed_callbacks[name] ||= begin
418
+ str = send("_#{kind}_callback").
419
+ compile(name, :object => obj, :terminator => send("_#{kind}_terminator"))
420
+
421
+ class_eval "def #{name}() #{str} end", __FILE__, __LINE__
422
+
423
+ true
424
+ end
425
+ end
426
+
427
+ # Define callbacks.
428
+ #
429
+ # Creates a <name>_callback method that you can use to add callbacks.
430
+ #
431
+ # Syntax:
432
+ # save_callback :before, :before_meth
433
+ # save_callback :after, :after_meth, :if => :condition
434
+ # save_callback :around {|r| stuff; yield; stuff }
435
+ #
436
+ # The <name>_callback method also updates the _run_<name>_callbacks
437
+ # method, which is the public API to run the callbacks.
438
+ #
439
+ # Also creates a skip_<name>_callback method that you can use to skip
440
+ # callbacks.
441
+ #
442
+ # When creating or skipping callbacks, you can specify conditions that
443
+ # are always the same for a given key. For instance, in ActionPack,
444
+ # we convert :only and :except conditions into per-key conditions.
445
+ #
446
+ # before_filter :authenticate, :except => "index"
447
+ # becomes
448
+ # dispatch_callback :before, :authenticate, :per_key => {:unless => proc {|c| c.action_name == "index"}}
449
+ #
450
+ # Per-Key conditions are evaluated only once per use of a given key.
451
+ # In the case of the above example, you would do:
452
+ #
453
+ # run_dispatch_callbacks(action_name) { ... dispatch stuff ... }
454
+ #
455
+ # In that case, each action_name would get its own compiled callback
456
+ # method that took into consideration the per_key conditions. This
457
+ # is a speed improvement for ActionPack.
458
+ def _update_callbacks(name, filters = CallbackChain.new(name), block = nil)
459
+ type = [:before, :after, :around].include?(filters.first) ? filters.shift : :before
460
+ options = filters.last.is_a?(Hash) ? filters.pop : {}
461
+ filters.unshift(block) if block
462
+
463
+ callbacks = send("_#{name}_callback")
464
+ yield callbacks, type, filters, options if block_given?
465
+
466
+ _define_runner(name)
467
+ end
468
+
469
+ alias_method :_reset_callbacks, :_update_callbacks
470
+
471
+ def set_callback(name, *filters, &block)
472
+ _update_callbacks(name, filters, block) do |callbacks, type, filters, options|
473
+ filters.map! do |filter|
474
+ # overrides parent class
475
+ callbacks.delete_if {|c| c.matches?(type, filter) }
476
+ Callback.new(filter, type, options.dup, self)
477
+ end
478
+
479
+ options[:prepend] ? callbacks.unshift(*filters) : callbacks.push(*filters)
480
+ end
481
+ end
482
+
483
+ def skip_callback(name, *filters, &block)
484
+ _update_callbacks(name, filters, block) do |callbacks, type, filters, options|
485
+ filters.each do |filter|
486
+ callbacks = send("_#{name}_callback=", callbacks.clone(self))
487
+
488
+ filter = callbacks.find {|c| c.matches?(type, filter) }
489
+
490
+ if filter && options.any?
491
+ filter.recompile!(options, options[:per_key] || {})
492
+ else
493
+ callbacks.delete(filter)
494
+ end
495
+ end
496
+ end
497
+ end
498
+
499
+ def define_callbacks(*symbols)
500
+ terminator = symbols.pop if symbols.last.is_a?(String)
501
+ symbols.each do |symbol|
502
+ couchrest_inheritable_accessor("_#{symbol}_terminator") { terminator }
503
+
504
+ couchrest_inheritable_accessor("_#{symbol}_callback") do
505
+ CallbackChain.new(symbol)
506
+ end
507
+
508
+ _define_runner(symbol)
509
+
510
+ # Define more convenient callback methods
511
+ # set_callback(:save, :before) becomes before_save
512
+ [:before, :after, :around].each do |filter|
513
+ self.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
514
+ def self.#{filter}_#{symbol}(*symbols, &blk)
515
+ _alias_callbacks(symbols, blk) do |callback, options|
516
+ set_callback(:#{symbol}, :#{filter}, callback, options)
517
+ end
518
+ end
519
+ RUBY_EVAL
520
+ end
521
+ end
522
+ end
523
+
524
+ def _alias_callbacks(callbacks, block)
525
+ options = callbacks.last.is_a?(Hash) ? callbacks.pop : {}
526
+ callbacks.push(block) if block
527
+ callbacks.each do |callback|
528
+ yield callback, options
529
+ end
530
+ end
531
+ end
532
+ end
533
+ end
534
+ end