couchrest 0.38 → 1.0.0.beta

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 (77) hide show
  1. data/README.md +8 -8
  2. data/Rakefile +3 -4
  3. data/couchrest.gemspec +25 -105
  4. data/history.txt +5 -4
  5. data/lib/couchrest.rb +31 -52
  6. data/lib/couchrest/{core/database.rb → database.rb} +6 -11
  7. data/lib/couchrest/{core/design.rb → design.rb} +2 -2
  8. data/lib/couchrest/{core/document.rb → document.rb} +1 -1
  9. data/lib/couchrest/helper/attachments.rb +29 -0
  10. data/lib/couchrest/middlewares/logger.rb +3 -3
  11. data/lib/couchrest/monkeypatches.rb +1 -71
  12. data/lib/couchrest/{core/response.rb → response.rb} +0 -0
  13. data/lib/couchrest/{core/rest_api.rb → rest_api.rb} +8 -12
  14. data/lib/couchrest/{core/server.rb → server.rb} +0 -2
  15. data/spec/couchrest/{core/couchrest_spec.rb → couchrest_spec.rb} +15 -9
  16. data/spec/couchrest/{core/database_spec.rb → database_spec.rb} +4 -4
  17. data/spec/couchrest/{core/design_spec.rb → design_spec.rb} +2 -2
  18. data/spec/couchrest/{core/document_spec.rb → document_spec.rb} +1 -1
  19. data/spec/couchrest/{core/server_spec.rb → server_spec.rb} +2 -2
  20. data/spec/spec.opts +0 -1
  21. data/spec/spec_helper.rb +0 -4
  22. metadata +32 -133
  23. data/examples/model/example.rb +0 -144
  24. data/lib/couchrest/core/adapters/restclient.rb +0 -35
  25. data/lib/couchrest/core/http_abstraction.rb +0 -48
  26. data/lib/couchrest/core/view.rb +0 -4
  27. data/lib/couchrest/mixins.rb +0 -4
  28. data/lib/couchrest/mixins/attachments.rb +0 -31
  29. data/lib/couchrest/mixins/attribute_protection.rb +0 -74
  30. data/lib/couchrest/mixins/callbacks.rb +0 -532
  31. data/lib/couchrest/mixins/class_proxy.rb +0 -124
  32. data/lib/couchrest/mixins/collection.rb +0 -260
  33. data/lib/couchrest/mixins/design_doc.rb +0 -103
  34. data/lib/couchrest/mixins/document_queries.rb +0 -80
  35. data/lib/couchrest/mixins/extended_attachments.rb +0 -70
  36. data/lib/couchrest/mixins/extended_document_mixins.rb +0 -9
  37. data/lib/couchrest/mixins/properties.rb +0 -158
  38. data/lib/couchrest/mixins/validation.rb +0 -246
  39. data/lib/couchrest/mixins/views.rb +0 -173
  40. data/lib/couchrest/more/casted_model.rb +0 -58
  41. data/lib/couchrest/more/extended_document.rb +0 -310
  42. data/lib/couchrest/more/property.rb +0 -58
  43. data/lib/couchrest/more/typecast.rb +0 -180
  44. data/lib/couchrest/support/blank.rb +0 -42
  45. data/lib/couchrest/support/rails.rb +0 -42
  46. data/lib/couchrest/validation/auto_validate.rb +0 -157
  47. data/lib/couchrest/validation/contextual_validators.rb +0 -78
  48. data/lib/couchrest/validation/validation_errors.rb +0 -125
  49. data/lib/couchrest/validation/validators/absent_field_validator.rb +0 -74
  50. data/lib/couchrest/validation/validators/confirmation_validator.rb +0 -107
  51. data/lib/couchrest/validation/validators/format_validator.rb +0 -122
  52. data/lib/couchrest/validation/validators/formats/email.rb +0 -66
  53. data/lib/couchrest/validation/validators/formats/url.rb +0 -43
  54. data/lib/couchrest/validation/validators/generic_validator.rb +0 -120
  55. data/lib/couchrest/validation/validators/length_validator.rb +0 -139
  56. data/lib/couchrest/validation/validators/method_validator.rb +0 -89
  57. data/lib/couchrest/validation/validators/numeric_validator.rb +0 -109
  58. data/lib/couchrest/validation/validators/required_field_validator.rb +0 -114
  59. data/spec/couchrest/more/attribute_protection_spec.rb +0 -150
  60. data/spec/couchrest/more/casted_extended_doc_spec.rb +0 -73
  61. data/spec/couchrest/more/casted_model_spec.rb +0 -406
  62. data/spec/couchrest/more/extended_doc_attachment_spec.rb +0 -135
  63. data/spec/couchrest/more/extended_doc_inherited_spec.rb +0 -40
  64. data/spec/couchrest/more/extended_doc_spec.rb +0 -807
  65. data/spec/couchrest/more/extended_doc_subclass_spec.rb +0 -98
  66. data/spec/couchrest/more/extended_doc_view_spec.rb +0 -456
  67. data/spec/couchrest/more/property_spec.rb +0 -628
  68. data/spec/fixtures/more/article.rb +0 -35
  69. data/spec/fixtures/more/card.rb +0 -22
  70. data/spec/fixtures/more/cat.rb +0 -20
  71. data/spec/fixtures/more/course.rb +0 -22
  72. data/spec/fixtures/more/event.rb +0 -8
  73. data/spec/fixtures/more/invoice.rb +0 -17
  74. data/spec/fixtures/more/person.rb +0 -9
  75. data/spec/fixtures/more/question.rb +0 -6
  76. data/spec/fixtures/more/service.rb +0 -12
  77. data/spec/fixtures/more/user.rb +0 -22
@@ -1,144 +0,0 @@
1
- require File.join(File.dirname(__FILE__), '..', '..', 'lib', 'couchrest')
2
-
3
- def show obj
4
- puts obj.inspect
5
- puts
6
- end
7
-
8
- SERVER = CouchRest.new
9
- SERVER.default_database = 'couchrest-extendeddoc-example'
10
-
11
- class Author < CouchRest::ExtendedDocument
12
- use_database SERVER.default_database
13
- property :name
14
-
15
- def drink_scotch
16
- puts "... glug type glug ... I'm #{name} ... type glug glug ..."
17
- end
18
- end
19
-
20
- class Post < CouchRest::ExtendedDocument
21
- use_database SERVER.default_database
22
-
23
- property :title
24
- property :body
25
- property :author, :cast_as => 'Author'
26
-
27
- timestamps!
28
- end
29
-
30
- class Comment < CouchRest::ExtendedDocument
31
- use_database SERVER.default_database
32
-
33
- property :commenter, :cast_as => 'Author'
34
- timestamps!
35
-
36
- def post= post
37
- self["post_id"] = post.id
38
- end
39
- def post
40
- Post.get(self['post_id']) if self['post_id']
41
- end
42
-
43
- end
44
-
45
- puts "Act I: CRUD"
46
- puts
47
- puts "(pause for dramatic effect)"
48
- puts
49
- sleep 2
50
-
51
- puts "Create an author."
52
- quentin = Author.new("name" => "Quentin Hazel")
53
- show quentin
54
-
55
- puts "Create a new post."
56
- post = Post.new(:title => "First Post", :body => "Lorem ipsum dolor sit amet, consectetur adipisicing elit...")
57
- show post
58
-
59
- puts "Add the author to the post."
60
- post.author = quentin
61
- show post
62
-
63
- puts "Save the post."
64
- post.save
65
- show post
66
-
67
- puts "Load the post."
68
- reloaded = Post.get(post.id)
69
- show reloaded
70
-
71
- puts "The author of the post is an instance of Author."
72
- reloaded.author.drink_scotch
73
-
74
- puts "\nAdd some comments to the post."
75
- comment_one = Comment.new :text => "Blah blah blah", :commenter => {:name => "Joe Sixpack"}
76
- comment_two = Comment.new :text => "Yeah yeah yeah", :commenter => {:name => "Jane Doe"}
77
- comment_three = Comment.new :text => "Whatever...", :commenter => {:name => "John Stewart"}
78
-
79
- # TODO - maybe add some magic here?
80
- comment_one.post = post
81
- comment_two.post = post
82
- comment_three.post = post
83
- comment_one.save
84
- comment_two.save
85
- comment_three.save
86
-
87
- show comment_one
88
- show comment_two
89
- show comment_three
90
-
91
- puts "We can load a post through its comment (no magic here)."
92
- show post = comment_one.post
93
-
94
- puts "Commenters are also authors."
95
- comment_two['commenter'].drink_scotch
96
- comment_one['commenter'].drink_scotch
97
- comment_three['commenter'].drink_scotch
98
-
99
- puts "\nLet's save an author to her own document."
100
- jane = comment_two['commenter']
101
- jane.save
102
- show jane
103
-
104
- puts "Oh, that's neat! Because Ruby passes hash valuee by reference, Jane's new id has been added to the comment she left."
105
- show comment_two
106
-
107
- puts "Of course, we'd better remember to save it."
108
- comment_two.save
109
- show comment_two
110
-
111
- puts "Oooh, denormalized... feel the burn!"
112
- puts
113
- puts
114
- puts
115
- puts "Act II: Views"
116
- puts
117
- puts
118
- sleep 2
119
-
120
- puts "Let's find all the comments that go with our post."
121
- puts "Our post has id #{post.id}, so lets find all the comments with that post_id."
122
- puts
123
-
124
- class Comment
125
- view_by :post_id
126
- end
127
-
128
- comments = Comment.by_post_id :key => post.id
129
- show comments
130
-
131
- puts "That was too easy."
132
- puts "We can even wrap it up in a finder on the Post class."
133
- puts
134
-
135
- class Post
136
- def comments
137
- Comment.by_post_id :key => id
138
- end
139
- end
140
-
141
- show post.comments
142
- puts "Gimme 5 minutes and I'll roll this into the framework. ;)"
143
- puts
144
- puts "There is a lot more that can be done with views, but a lot of the interesting stuff is joins, which of course range across types. We'll pick up where we left off, next time."
@@ -1,35 +0,0 @@
1
- module RestClientAdapter
2
-
3
- module API
4
- def proxy=(url)
5
- RestClient.proxy = url
6
- end
7
-
8
- def proxy
9
- RestClient.proxy
10
- end
11
-
12
- def get(uri, headers={})
13
- RestClient.get(uri, headers).to_s
14
- end
15
-
16
- def post(uri, payload, headers={})
17
- RestClient.post(uri, payload, headers).to_s
18
- end
19
-
20
- def put(uri, payload, headers={})
21
- RestClient.put(uri, payload, headers).to_s
22
- end
23
-
24
- def delete(uri, headers={})
25
- RestClient.delete(uri, headers).to_s
26
- end
27
-
28
- def copy(uri, headers)
29
- RestClient::Request.execute( :method => :copy,
30
- :url => uri,
31
- :headers => headers).to_s
32
- end
33
- end
34
-
35
- end
@@ -1,48 +0,0 @@
1
- require 'couchrest/core/adapters/restclient'
2
-
3
- # Abstraction layet for HTTP communications.
4
- #
5
- # By defining a basic API that CouchRest is relying on,
6
- # it allows for easy experimentations and implementations of various libraries.
7
- #
8
- # Most of the API is based on the RestClient API that was used in the early version of CouchRest.
9
- #
10
- module HttpAbstraction
11
-
12
- # here is the list of exception expected by CouchRest
13
- # please convert the underlying errors in this set of known
14
- # exceptions.
15
- class ResourceNotFound < StandardError; end
16
- class RequestFailed < StandardError; end
17
- class RequestTimeout < StandardError; end
18
- class ServerBrokeConnection < StandardError; end
19
- class Conflict < StandardError; end
20
-
21
-
22
- # # Here is the API you need to implement if you want to write a new adapter
23
- # # See adapters/restclient.rb for more information.
24
- #
25
- # def self.proxy=(url)
26
- # end
27
- #
28
- # def self.proxy
29
- # end
30
- #
31
- # def self.get(uri, headers=nil)
32
- # end
33
- #
34
- # def self.post(uri, payload, headers=nil)
35
- # end
36
- #
37
- # def self.put(uri, payload, headers=nil)
38
- # end
39
- #
40
- # def self.delete(uri, headers=nil)
41
- # end
42
- #
43
- # def self.copy(uri, headers)
44
- # end
45
-
46
- end
47
-
48
- HttpAbstraction.extend(RestClientAdapter::API)
@@ -1,4 +0,0 @@
1
- module CouchRest
2
- class View
3
- end
4
- end
@@ -1,4 +0,0 @@
1
- mixins_dir = File.join(File.dirname(__FILE__), 'mixins')
2
-
3
- require File.join(mixins_dir, 'attachments')
4
- require File.join(mixins_dir, 'callbacks')
@@ -1,31 +0,0 @@
1
- module CouchRest
2
- module Mixins
3
- module Attachments
4
-
5
- # saves an attachment directly to couchdb
6
- def put_attachment(name, file, options={})
7
- raise ArgumentError, "doc must be saved" unless self.rev
8
- raise ArgumentError, "doc.database required to put_attachment" unless database
9
- result = database.put_attachment(self, name, file, options)
10
- self['_rev'] = result['rev']
11
- result['ok']
12
- end
13
-
14
- # returns an attachment's data
15
- def fetch_attachment(name)
16
- raise ArgumentError, "doc must be saved" unless self.rev
17
- raise ArgumentError, "doc.database required to put_attachment" unless database
18
- database.fetch_attachment(self, name)
19
- end
20
-
21
- # deletes an attachment directly from couchdb
22
- def delete_attachment(name, force=false)
23
- raise ArgumentError, "doc.database required to delete_attachment" unless database
24
- result = database.delete_attachment(self, name, force)
25
- self['_rev'] = result['rev']
26
- result['ok']
27
- end
28
-
29
- end
30
- end
31
- end
@@ -1,74 +0,0 @@
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,532 +0,0 @@
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
- require File.join(File.dirname(__FILE__), '..', 'support', 'class')
27
-
28
- module CouchRest
29
- # Callbacks are hooks into the lifecycle of an object that allow you to trigger logic
30
- # before or after an alteration of the object state.
31
- #
32
- # Mixing in this module allows you to define callbacks in your class.
33
- #
34
- # Example:
35
- # class Storage
36
- # include ActiveSupport::Callbacks
37
- #
38
- # define_callbacks :save
39
- # end
40
- #
41
- # class ConfigStorage < Storage
42
- # save_callback :before, :saving_message
43
- # def saving_message
44
- # puts "saving..."
45
- # end
46
- #
47
- # save_callback :after do |object|
48
- # puts "saved"
49
- # end
50
- #
51
- # def save
52
- # _run_save_callbacks do
53
- # puts "- save"
54
- # end
55
- # end
56
- # end
57
- #
58
- # config = ConfigStorage.new
59
- # config.save
60
- #
61
- # Output:
62
- # saving...
63
- # - save
64
- # saved
65
- #
66
- # Callbacks from parent classes are inherited.
67
- #
68
- # Example:
69
- # class Storage
70
- # include ActiveSupport::Callbacks
71
- #
72
- # define_callbacks :save
73
- #
74
- # save_callback :before, :prepare
75
- # def prepare
76
- # puts "preparing save"
77
- # end
78
- # end
79
- #
80
- # class ConfigStorage < Storage
81
- # save_callback :before, :saving_message
82
- # def saving_message
83
- # puts "saving..."
84
- # end
85
- #
86
- # save_callback :after do |object|
87
- # puts "saved"
88
- # end
89
- #
90
- # def save
91
- # _run_save_callbacks do
92
- # puts "- save"
93
- # end
94
- # end
95
- # end
96
- #
97
- # config = ConfigStorage.new
98
- # config.save
99
- #
100
- # Output:
101
- # preparing save
102
- # saving...
103
- # - save
104
- # saved
105
- module Callbacks
106
- def self.included(klass)
107
- klass.extend ClassMethods
108
- end
109
-
110
- def run_callbacks(kind, options = {}, &blk)
111
- send("_run_#{kind}_callbacks", &blk)
112
- end
113
-
114
- class Callback
115
- @@_callback_sequence = 0
116
-
117
- attr_accessor :filter, :kind, :name, :options, :per_key, :klass
118
- def initialize(filter, kind, options, klass)
119
- @kind, @klass = kind, klass
120
-
121
- normalize_options!(options)
122
-
123
- @per_key = options.delete(:per_key)
124
- @raw_filter, @options = filter, options
125
- @filter = _compile_filter(filter)
126
- @compiled_options = _compile_options(options)
127
- @callback_id = next_id
128
-
129
- _compile_per_key_options
130
- end
131
-
132
- def clone(klass)
133
- obj = super()
134
- obj.klass = klass
135
- obj.per_key = @per_key.dup
136
- obj.options = @options.dup
137
- obj.per_key[:if] = @per_key[:if].dup
138
- obj.per_key[:unless] = @per_key[:unless].dup
139
- obj.options[:if] = @options[:if].dup
140
- obj.options[:unless] = @options[:unless].dup
141
- obj
142
- end
143
-
144
- def normalize_options!(options)
145
- options[:if] = Array.wrap(options[:if])
146
- options[:unless] = Array.wrap(options[:unless])
147
-
148
- options[:per_key] ||= {}
149
- options[:per_key][:if] = Array.wrap(options[:per_key][:if])
150
- options[:per_key][:unless] = Array.wrap(options[:per_key][:unless])
151
- end
152
-
153
- def next_id
154
- @@_callback_sequence += 1
155
- end
156
-
157
- def matches?(_kind, _filter)
158
- @kind == _kind &&
159
- @filter == _filter
160
- end
161
-
162
- def _update_filter(filter_options, new_options)
163
- filter_options[:if].push(new_options[:unless]) if new_options.key?(:unless)
164
- filter_options[:unless].push(new_options[:if]) if new_options.key?(:if)
165
- end
166
-
167
- def recompile!(_options, _per_key)
168
- _update_filter(self.options, _options)
169
- _update_filter(self.per_key, _per_key)
170
-
171
- @callback_id = next_id
172
- @filter = _compile_filter(@raw_filter)
173
- @compiled_options = _compile_options(@options)
174
- _compile_per_key_options
175
- end
176
-
177
- def _compile_per_key_options
178
- key_options = _compile_options(@per_key)
179
-
180
- @klass.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
181
- def _one_time_conditions_valid_#{@callback_id}?
182
- true #{key_options[0]}
183
- end
184
- RUBY_EVAL
185
- end
186
-
187
- # This will supply contents for before and around filters, and no
188
- # contents for after filters (for the forward pass).
189
- def start(key = nil, options = {})
190
- object, terminator = (options || {}).values_at(:object, :terminator)
191
-
192
- return if key && !object.send("_one_time_conditions_valid_#{@callback_id}?")
193
-
194
- terminator ||= false
195
-
196
- # options[0] is the compiled form of supplied conditions
197
- # options[1] is the "end" for the conditional
198
-
199
- if @kind == :before || @kind == :around
200
- if @kind == :before
201
- # if condition # before_save :filter_name, :if => :condition
202
- # filter_name
203
- # end
204
- filter = <<-RUBY_EVAL
205
- unless halted
206
- result = #{@filter}
207
- halted = (#{terminator})
208
- end
209
- RUBY_EVAL
210
-
211
- [@compiled_options[0], filter, @compiled_options[1]].compact.join("\n")
212
- else
213
- # Compile around filters with conditions into proxy methods
214
- # that contain the conditions.
215
- #
216
- # For `around_save :filter_name, :if => :condition':
217
- #
218
- # def _conditional_callback_save_17
219
- # if condition
220
- # filter_name do
221
- # yield self
222
- # end
223
- # else
224
- # yield self
225
- # end
226
- # end
227
-
228
- name = "_conditional_callback_#{@kind}_#{next_id}"
229
- txt, line = <<-RUBY_EVAL, __LINE__ + 1
230
- def #{name}(halted)
231
- #{@compiled_options[0] || "if true"} && !halted
232
- #{@filter} do
233
- yield self
234
- end
235
- else
236
- yield self
237
- end
238
- end
239
- RUBY_EVAL
240
- @klass.class_eval(txt, __FILE__, line)
241
- "#{name}(halted) do"
242
- end
243
- end
244
- end
245
-
246
- # This will supply contents for around and after filters, but not
247
- # before filters (for the backward pass).
248
- def end(key = nil, options = {})
249
- object = (options || {})[:object]
250
-
251
- return if key && !object.send("_one_time_conditions_valid_#{@callback_id}?")
252
-
253
- if @kind == :around || @kind == :after
254
- # if condition # after_save :filter_name, :if => :condition
255
- # filter_name
256
- # end
257
- if @kind == :after
258
- [@compiled_options[0], @filter, @compiled_options[1]].compact.join("\n")
259
- else
260
- "end"
261
- end
262
- end
263
- end
264
-
265
- private
266
- # Options support the same options as filters themselves (and support
267
- # symbols, string, procs, and objects), so compile a conditional
268
- # expression based on the options
269
- def _compile_options(options)
270
- return [] if options[:if].empty? && options[:unless].empty?
271
-
272
- conditions = []
273
-
274
- unless options[:if].empty?
275
- conditions << Array.wrap(_compile_filter(options[:if]))
276
- end
277
-
278
- unless options[:unless].empty?
279
- conditions << Array.wrap(_compile_filter(options[:unless])).map {|f| "!#{f}"}
280
- end
281
-
282
- ["if #{conditions.flatten.join(" && ")}", "end"]
283
- end
284
-
285
- # Filters support:
286
- # Arrays:: Used in conditions. This is used to specify
287
- # multiple conditions. Used internally to
288
- # merge conditions from skip_* filters
289
- # Symbols:: A method to call
290
- # Strings:: Some content to evaluate
291
- # Procs:: A proc to call with the object
292
- # Objects:: An object with a before_foo method on it to call
293
- #
294
- # All of these objects are compiled into methods and handled
295
- # the same after this point:
296
- # Arrays:: Merged together into a single filter
297
- # Symbols:: Already methods
298
- # Strings:: class_eval'ed into methods
299
- # Procs:: define_method'ed into methods
300
- # Objects::
301
- # a method is created that calls the before_foo method
302
- # on the object.
303
- def _compile_filter(filter)
304
- method_name = "_callback_#{@kind}_#{next_id}"
305
- case filter
306
- when Array
307
- filter.map {|f| _compile_filter(f)}
308
- when Symbol
309
- filter
310
- when String
311
- "(#{filter})"
312
- when Proc
313
- @klass.send(:define_method, method_name, &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)
325
- end
326
- RUBY_EVAL
327
-
328
- method_name
329
- end
330
- end
331
-
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
344
- end
345
-
346
- end
347
-
348
- # An Array with a compile method
349
- class CallbackChain < Array
350
- def initialize(symbol)
351
- @symbol = symbol
352
- end
353
-
354
- def compile(key = nil, options = {})
355
- method = []
356
- method << "halted = false"
357
- each do |callback|
358
- method << callback.start(key, options)
359
- end
360
- method << "yield self if block_given? && !halted"
361
- reverse_each do |callback|
362
- method << callback.end(key, options)
363
- end
364
- method.compact.join("\n")
365
- end
366
-
367
- def clone(klass)
368
- chain = CallbackChain.new(@symbol)
369
- chain.push(*map {|c| c.clone(klass)})
370
- end
371
- end
372
-
373
- module ClassMethods
374
- #CHAINS = {:before => :before, :around => :before, :after => :after}
375
-
376
- # Make the _run_save_callbacks method. The generated method takes
377
- # a block that it'll yield to. It'll call the before and around filters
378
- # in order, yield the block, and then run the after filters.
379
- #
380
- # _run_save_callbacks do
381
- # save
382
- # end
383
- #
384
- # The _run_save_callbacks method can optionally take a key, which
385
- # will be used to compile an optimized callback method for each
386
- # key. See #define_callbacks for more information.
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)
393
- if key
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)
401
- else
402
- #{body}
403
- end
404
- end
405
- RUBY_EVAL
406
-
407
- undef_method "_run_#{symbol}_callbacks" if method_defined?("_run_#{symbol}_callbacks")
408
- class_eval body, __FILE__, line
409
- end
410
-
411
- # This is called the first time a callback is called with a particular
412
- # key. It creates a new callback method for the key, calculating
413
- # which callbacks can be omitted because of per_key conditions.
414
- def _create_keyed_callback(name, kind, obj, &blk)
415
- @_keyed_callbacks ||= {}
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__
421
-
422
- true
423
- end
424
- end
425
-
426
- # Define callbacks.
427
- #
428
- # Creates a <name>_callback method that you can use to add callbacks.
429
- #
430
- # Syntax:
431
- # save_callback :before, :before_meth
432
- # save_callback :after, :after_meth, :if => :condition
433
- # save_callback :around {|r| stuff; yield; stuff }
434
- #
435
- # The <name>_callback method also updates the _run_<name>_callbacks
436
- # method, which is the public API to run the callbacks.
437
- #
438
- # Also creates a skip_<name>_callback method that you can use to skip
439
- # callbacks.
440
- #
441
- # When creating or skipping callbacks, you can specify conditions that
442
- # are always the same for a given key. For instance, in ActionPack,
443
- # we convert :only and :except conditions into per-key conditions.
444
- #
445
- # before_filter :authenticate, :except => "index"
446
- # becomes
447
- # dispatch_callback :before, :authenticate, :per_key => {:unless => proc {|c| c.action_name == "index"}}
448
- #
449
- # Per-Key conditions are evaluated only once per use of a given key.
450
- # In the case of the above example, you would do:
451
- #
452
- # run_dispatch_callbacks(action_name) { ... dispatch stuff ... }
453
- #
454
- # In that case, each action_name would get its own compiled callback
455
- # method that took into consideration the per_key conditions. This
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
-
498
- def define_callbacks(*symbols)
499
- terminator = symbols.pop if symbols.last.is_a?(String)
500
- symbols.each do |symbol|
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)
516
- end
517
- end
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
528
- end
529
- end
530
- end
531
- end
532
- end