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.
- data/LICENSE +176 -0
- data/README.md +250 -0
- data/Rakefile +69 -0
- data/THANKS.md +22 -0
- data/examples/model/example.rb +144 -0
- data/history.txt +165 -0
- data/lib/couchrest/casted_array.rb +39 -0
- data/lib/couchrest/casted_model.rb +53 -0
- data/lib/couchrest/extended_document.rb +262 -0
- data/lib/couchrest/mixins.rb +12 -0
- data/lib/couchrest/mixins/attribute_protection.rb +74 -0
- data/lib/couchrest/mixins/attributes.rb +75 -0
- data/lib/couchrest/mixins/callbacks.rb +534 -0
- data/lib/couchrest/mixins/class_proxy.rb +120 -0
- data/lib/couchrest/mixins/collection.rb +260 -0
- data/lib/couchrest/mixins/design_doc.rb +159 -0
- data/lib/couchrest/mixins/document_queries.rb +82 -0
- data/lib/couchrest/mixins/extended_attachments.rb +73 -0
- data/lib/couchrest/mixins/properties.rb +130 -0
- data/lib/couchrest/mixins/typecast.rb +174 -0
- data/lib/couchrest/mixins/views.rb +148 -0
- data/lib/couchrest/property.rb +96 -0
- data/lib/couchrest/support/couchrest.rb +19 -0
- data/lib/couchrest/support/rails.rb +42 -0
- data/lib/couchrest/validation.rb +246 -0
- data/lib/couchrest/validation/auto_validate.rb +156 -0
- data/lib/couchrest/validation/contextual_validators.rb +78 -0
- data/lib/couchrest/validation/validation_errors.rb +125 -0
- data/lib/couchrest/validation/validators/absent_field_validator.rb +74 -0
- data/lib/couchrest/validation/validators/confirmation_validator.rb +107 -0
- data/lib/couchrest/validation/validators/format_validator.rb +122 -0
- data/lib/couchrest/validation/validators/formats/email.rb +66 -0
- data/lib/couchrest/validation/validators/formats/url.rb +43 -0
- data/lib/couchrest/validation/validators/generic_validator.rb +120 -0
- data/lib/couchrest/validation/validators/length_validator.rb +139 -0
- data/lib/couchrest/validation/validators/method_validator.rb +89 -0
- data/lib/couchrest/validation/validators/numeric_validator.rb +109 -0
- data/lib/couchrest/validation/validators/required_field_validator.rb +114 -0
- data/lib/couchrest_extended_document.rb +23 -0
- data/spec/couchrest/attribute_protection_spec.rb +150 -0
- data/spec/couchrest/casted_extended_doc_spec.rb +79 -0
- data/spec/couchrest/casted_model_spec.rb +424 -0
- data/spec/couchrest/extended_doc_attachment_spec.rb +148 -0
- data/spec/couchrest/extended_doc_inherited_spec.rb +40 -0
- data/spec/couchrest/extended_doc_spec.rb +869 -0
- data/spec/couchrest/extended_doc_subclass_spec.rb +101 -0
- data/spec/couchrest/extended_doc_view_spec.rb +529 -0
- data/spec/couchrest/property_spec.rb +790 -0
- data/spec/fixtures/attachments/README +3 -0
- data/spec/fixtures/attachments/couchdb.png +0 -0
- data/spec/fixtures/attachments/test.html +11 -0
- data/spec/fixtures/more/article.rb +35 -0
- data/spec/fixtures/more/card.rb +22 -0
- data/spec/fixtures/more/cat.rb +22 -0
- data/spec/fixtures/more/course.rb +25 -0
- data/spec/fixtures/more/event.rb +8 -0
- data/spec/fixtures/more/invoice.rb +17 -0
- data/spec/fixtures/more/person.rb +9 -0
- data/spec/fixtures/more/question.rb +7 -0
- data/spec/fixtures/more/service.rb +12 -0
- data/spec/fixtures/more/user.rb +22 -0
- data/spec/fixtures/views/lib.js +3 -0
- data/spec/fixtures/views/test_view/lib.js +3 -0
- data/spec/fixtures/views/test_view/only-map.js +4 -0
- data/spec/fixtures/views/test_view/test-map.js +3 -0
- data/spec/fixtures/views/test_view/test-reduce.js +3 -0
- data/spec/spec.opts +5 -0
- data/spec/spec_helper.rb +49 -0
- data/utils/remap.rb +27 -0
- data/utils/subset.rb +30 -0
- 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
|