couchrest 0.12.4 → 0.23

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 (64) hide show
  1. data/README.md +33 -8
  2. data/Rakefile +11 -2
  3. data/examples/model/example.rb +19 -13
  4. data/lib/couchrest.rb +70 -11
  5. data/lib/couchrest/core/database.rb +121 -62
  6. data/lib/couchrest/core/design.rb +7 -17
  7. data/lib/couchrest/core/document.rb +42 -30
  8. data/lib/couchrest/core/response.rb +16 -0
  9. data/lib/couchrest/core/server.rb +47 -10
  10. data/lib/couchrest/helper/upgrade.rb +51 -0
  11. data/lib/couchrest/mixins.rb +4 -0
  12. data/lib/couchrest/mixins/attachments.rb +31 -0
  13. data/lib/couchrest/mixins/callbacks.rb +483 -0
  14. data/lib/couchrest/mixins/class_proxy.rb +108 -0
  15. data/lib/couchrest/mixins/design_doc.rb +90 -0
  16. data/lib/couchrest/mixins/document_queries.rb +44 -0
  17. data/lib/couchrest/mixins/extended_attachments.rb +68 -0
  18. data/lib/couchrest/mixins/extended_document_mixins.rb +7 -0
  19. data/lib/couchrest/mixins/properties.rb +129 -0
  20. data/lib/couchrest/mixins/validation.rb +242 -0
  21. data/lib/couchrest/mixins/views.rb +169 -0
  22. data/lib/couchrest/monkeypatches.rb +81 -6
  23. data/lib/couchrest/more/casted_model.rb +28 -0
  24. data/lib/couchrest/more/extended_document.rb +215 -0
  25. data/lib/couchrest/more/property.rb +40 -0
  26. data/lib/couchrest/support/blank.rb +42 -0
  27. data/lib/couchrest/support/class.rb +176 -0
  28. data/lib/couchrest/validation/auto_validate.rb +163 -0
  29. data/lib/couchrest/validation/contextual_validators.rb +78 -0
  30. data/lib/couchrest/validation/validation_errors.rb +118 -0
  31. data/lib/couchrest/validation/validators/absent_field_validator.rb +74 -0
  32. data/lib/couchrest/validation/validators/confirmation_validator.rb +99 -0
  33. data/lib/couchrest/validation/validators/format_validator.rb +117 -0
  34. data/lib/couchrest/validation/validators/formats/email.rb +66 -0
  35. data/lib/couchrest/validation/validators/formats/url.rb +43 -0
  36. data/lib/couchrest/validation/validators/generic_validator.rb +120 -0
  37. data/lib/couchrest/validation/validators/length_validator.rb +134 -0
  38. data/lib/couchrest/validation/validators/method_validator.rb +89 -0
  39. data/lib/couchrest/validation/validators/numeric_validator.rb +104 -0
  40. data/lib/couchrest/validation/validators/required_field_validator.rb +109 -0
  41. data/spec/couchrest/core/database_spec.rb +189 -124
  42. data/spec/couchrest/core/design_spec.rb +13 -6
  43. data/spec/couchrest/core/document_spec.rb +231 -177
  44. data/spec/couchrest/core/server_spec.rb +35 -0
  45. data/spec/couchrest/helpers/pager_spec.rb +1 -1
  46. data/spec/couchrest/more/casted_extended_doc_spec.rb +40 -0
  47. data/spec/couchrest/more/casted_model_spec.rb +98 -0
  48. data/spec/couchrest/more/extended_doc_attachment_spec.rb +130 -0
  49. data/spec/couchrest/more/extended_doc_spec.rb +509 -0
  50. data/spec/couchrest/more/extended_doc_subclass_spec.rb +98 -0
  51. data/spec/couchrest/more/extended_doc_view_spec.rb +355 -0
  52. data/spec/couchrest/more/property_spec.rb +136 -0
  53. data/spec/fixtures/more/article.rb +34 -0
  54. data/spec/fixtures/more/card.rb +20 -0
  55. data/spec/fixtures/more/course.rb +14 -0
  56. data/spec/fixtures/more/event.rb +6 -0
  57. data/spec/fixtures/more/invoice.rb +17 -0
  58. data/spec/fixtures/more/person.rb +8 -0
  59. data/spec/fixtures/more/question.rb +6 -0
  60. data/spec/fixtures/more/service.rb +12 -0
  61. data/spec/spec_helper.rb +13 -7
  62. metadata +58 -4
  63. data/lib/couchrest/core/model.rb +0 -613
  64. data/spec/couchrest/core/model_spec.rb +0 -855
@@ -35,11 +35,17 @@ JAVASCRIPT
35
35
  end
36
36
 
37
37
  # Dispatches to any named view.
38
+ # (using the database where this design doc was saved)
38
39
  def view view_name, query={}, &block
40
+ view_on database, view_name, query, &block
41
+ end
42
+
43
+ # Dispatches to any named view in a specific database
44
+ def view_on db, view_name, query={}, &block
39
45
  view_name = view_name.to_s
40
46
  view_slug = "#{name}/#{view_name}"
41
47
  defaults = (self['views'][view_name] && self['views'][view_name]["couchrest-defaults"]) || {}
42
- fetch_view(view_slug, defaults.merge(query), &block)
48
+ db.view(view_slug, defaults.merge(query), &block)
43
49
  end
44
50
 
45
51
  def name
@@ -64,22 +70,6 @@ JAVASCRIPT
64
70
  (self['views'][view]["couchrest-defaults"]||{})
65
71
  end
66
72
 
67
- # def fetch_view_with_docs name, opts, raw=false, &block
68
- # if raw
69
- # fetch_view name, opts, &block
70
- # else
71
- # begin
72
- # view = fetch_view name, opts.merge({:include_docs => true}), &block
73
- # view['rows'].collect{|r|new(r['doc'])} if view['rows']
74
- # rescue
75
- # # fallback for old versions of couchdb that don't
76
- # # have include_docs support
77
- # view = fetch_view name, opts, &block
78
- # view['rows'].collect{|r|new(database.get(r['id']))} if view['rows']
79
- # end
80
- # end
81
- # end
82
-
83
73
  def fetch_view view_name, opts, &block
84
74
  database.view(view_name, opts, &block)
85
75
  end
@@ -1,44 +1,43 @@
1
- module CouchRest
2
- class Response < Hash
3
- def initialize keys = {}
4
- keys.each do |k,v|
5
- self[k.to_s] = v
6
- end
7
- end
8
- def []= key, value
9
- super(key.to_s, value)
10
- end
11
- def [] key
12
- super(key.to_s)
13
- end
14
- end
15
-
1
+ require 'delegate'
2
+
3
+ module CouchRest
16
4
  class Document < Response
5
+ include CouchRest::Mixins::Attachments
17
6
 
7
+ # def self.inherited(subklass)
8
+ # subklass.send(:extlib_inheritable_accessor, :database)
9
+ # end
10
+
11
+ extlib_inheritable_accessor :database
18
12
  attr_accessor :database
19
-
20
- # alias for self['_id']
13
+
14
+ # override the CouchRest::Model-wide default_database
15
+ # This is not a thread safe operation, do not change the model
16
+ # database at runtime.
17
+ def self.use_database(db)
18
+ self.database = db
19
+ end
20
+
21
21
  def id
22
22
  self['_id']
23
23
  end
24
-
25
- # alias for self['_rev']
24
+
26
25
  def rev
27
26
  self['_rev']
28
27
  end
29
-
28
+
30
29
  # returns true if the document has never been saved
31
30
  def new_document?
32
31
  !rev
33
32
  end
34
-
33
+
35
34
  # Saves the document to the db using create or update. Also runs the :save
36
35
  # callbacks. Sets the <tt>_id</tt> and <tt>_rev</tt> fields based on
37
36
  # CouchDB's response.
38
37
  # If <tt>bulk</tt> is <tt>true</tt> (defaults to false) the document is cached for bulk save.
39
38
  def save(bulk = false)
40
39
  raise ArgumentError, "doc.database required for saving" unless database
41
- result = database.save self, bulk
40
+ result = database.save_doc self, bulk
42
41
  result['ok']
43
42
  end
44
43
 
@@ -49,7 +48,7 @@ module CouchRest
49
48
  # actually be deleted from the db until bulk save.
50
49
  def destroy(bulk = false)
51
50
  raise ArgumentError, "doc.database required to destroy" unless database
52
- result = database.delete(self, bulk)
51
+ result = database.delete_doc(self, bulk)
53
52
  if result['ok']
54
53
  self['_rev'] = nil
55
54
  self['_id'] = nil
@@ -57,19 +56,32 @@ module CouchRest
57
56
  result['ok']
58
57
  end
59
58
 
59
+ # copies the document to a new id. If the destination id currently exists, a rev must be provided.
60
+ # <tt>dest</tt> can take one of two forms if overwriting: "id_to_overwrite?rev=revision" or the actual doc
61
+ # hash with a '_rev' key
60
62
  def copy(dest)
61
63
  raise ArgumentError, "doc.database required to copy" unless database
62
- result = database.copy(self, dest)
64
+ result = database.copy_doc(self, dest)
63
65
  result['ok']
64
66
  end
65
67
 
66
- def move(dest)
67
- raise ArgumentError, "doc.database required to copy" unless database
68
- result = database.move(self, dest)
69
- result['ok']
68
+ # Returns the CouchDB uri for the document
69
+ def uri(append_rev = false)
70
+ return nil if new_document?
71
+ couch_uri = "http://#{database.uri}/#{CGI.escape(id)}"
72
+ if append_rev == true
73
+ couch_uri << "?rev=#{rev}"
74
+ elsif append_rev.kind_of?(Integer)
75
+ couch_uri << "?rev=#{append_rev}"
76
+ end
77
+ couch_uri
70
78
  end
71
-
79
+
80
+ # Returns the document's database
81
+ def database
82
+ @database || self.class.database
83
+ end
84
+
72
85
  end
73
-
74
86
 
75
87
  end
@@ -0,0 +1,16 @@
1
+ module CouchRest
2
+ class Response < Hash
3
+ def initialize(pkeys = {})
4
+ pkeys ||= {}
5
+ pkeys.each do |k,v|
6
+ self[k.to_s] = v
7
+ end
8
+ end
9
+ def []=(key, value)
10
+ super(key.to_s, value)
11
+ end
12
+ def [](key)
13
+ super(key.to_s)
14
+ end
15
+ end
16
+ end
@@ -1,25 +1,62 @@
1
1
  module CouchRest
2
2
  class Server
3
- attr_accessor :uri, :uuid_batch_count
4
- def initialize server = 'http://127.0.0.1:5984', uuid_batch_count = 1000
3
+ attr_accessor :uri, :uuid_batch_count, :available_databases
4
+ def initialize(server = 'http://127.0.0.1:5984', uuid_batch_count = 1000)
5
5
  @uri = server
6
6
  @uuid_batch_count = uuid_batch_count
7
7
  end
8
8
 
9
- # List all databases on the server
9
+ # Lists all "available" databases.
10
+ # An available database, is a database that was specified
11
+ # as avaiable by your code.
12
+ # It allows to define common databases to use and reuse in your code
13
+ def available_databases
14
+ @available_databases ||= {}
15
+ end
16
+
17
+ # Adds a new available database and create it unless it already exists
18
+ #
19
+ # Example:
20
+ #
21
+ # @couch = CouchRest::Server.new
22
+ # @couch.define_available_database(:default, "tech-blog")
23
+ #
24
+ def define_available_database(reference, db_name, create_unless_exists = true)
25
+ available_databases[reference.to_sym] = create_unless_exists ? database!(db_name) : database(db_name)
26
+ end
27
+
28
+ # Checks that a database is set as available
29
+ #
30
+ # Example:
31
+ #
32
+ # @couch.available_database?(:default)
33
+ #
34
+ def available_database?(ref_or_name)
35
+ ref_or_name.is_a?(Symbol) ? available_databases.keys.include?(ref_or_name) : available_databases.values.map{|db| db.name}.include?(ref_or_name)
36
+ end
37
+
38
+ def default_database=(name, create_unless_exists = true)
39
+ define_available_database(:default, name, create_unless_exists = true)
40
+ end
41
+
42
+ def default_database
43
+ available_databases[:default]
44
+ end
45
+
46
+ # Lists all databases on the server
10
47
  def databases
11
48
  CouchRest.get "#{@uri}/_all_dbs"
12
49
  end
13
50
 
14
51
  # Returns a CouchRest::Database for the given name
15
- def database name
52
+ def database(name)
16
53
  CouchRest::Database.new(self, name)
17
54
  end
18
55
 
19
56
  # Creates the database if it doesn't exist
20
- def database! name
57
+ def database!(name)
21
58
  create_db(name) rescue nil
22
- database name
59
+ database(name)
23
60
  end
24
61
 
25
62
  # GET the welcome message
@@ -28,9 +65,9 @@ module CouchRest
28
65
  end
29
66
 
30
67
  # Create a database
31
- def create_db name
68
+ def create_db(name)
32
69
  CouchRest.put "#{@uri}/#{name}"
33
- database name
70
+ database(name)
34
71
  end
35
72
 
36
73
  # Restart the CouchDB instance
@@ -39,10 +76,10 @@ module CouchRest
39
76
  end
40
77
 
41
78
  # Retrive an unused UUID from CouchDB. Server instances manage caching a list of unused UUIDs.
42
- def next_uuid count = @uuid_batch_count
79
+ def next_uuid(count = @uuid_batch_count)
43
80
  @uuids ||= []
44
81
  if @uuids.empty?
45
- @uuids = CouchRest.post("#{@uri}/_uuids?count=#{count}")["uuids"]
82
+ @uuids = CouchRest.get("#{@uri}/_uuids?count=#{count}")["uuids"]
46
83
  end
47
84
  @uuids.pop
48
85
  end
@@ -0,0 +1,51 @@
1
+ module CouchRest
2
+ class Upgrade
3
+ attr_accessor :olddb, :newdb, :dbname
4
+ def initialize dbname, old_couch, new_couch
5
+ @dbname = dbname
6
+ @olddb = old_couch.database dbname
7
+ @newdb = new_couch.database!(dbname)
8
+ @bulk_docs = []
9
+ end
10
+ def clone!
11
+ puts "#{dbname} - #{olddb.info['doc_count']} docs"
12
+ streamer = CouchRest::Streamer.new(olddb)
13
+ streamer.view("_all_docs_by_seq") do |row|
14
+ load_row_docs(row) if row
15
+ maybe_flush_bulks
16
+ end
17
+ flush_bulks!
18
+ end
19
+
20
+ private
21
+
22
+ def maybe_flush_bulks
23
+ flush_bulks! if (@bulk_docs.length > 99)
24
+ end
25
+
26
+ def flush_bulks!
27
+ url = CouchRest.paramify_url "#{@newdb.uri}/_bulk_docs", {:all_or_nothing => true}
28
+ puts "posting #{@bulk_docs.length} bulk docs to #{url}"
29
+ begin
30
+ CouchRest.post url, {:docs => @bulk_docs}
31
+ @bulk_docs = []
32
+ rescue Exception => e
33
+ puts e.response
34
+ raise e
35
+ end
36
+ end
37
+
38
+ def load_row_docs(row)
39
+ results = @olddb.get(row["id"], {:open_revs => "all", :attachments => true})
40
+ results.select{|r|r["ok"]}.each do |r|
41
+ doc = r["ok"]
42
+ if /^_/.match(doc["_id"]) && !/^_design/.match(doc["_id"])
43
+ puts "invalid docid #{doc["_id"]} -- trimming"
44
+ doc["_id"] = doc["_id"].sub('_','')
45
+ end
46
+ doc.delete('_rev')
47
+ @bulk_docs << doc
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,4 @@
1
+ mixins_dir = File.join(File.dirname(__FILE__), 'mixins')
2
+
3
+ require File.join(mixins_dir, 'attachments')
4
+ require File.join(mixins_dir, 'callbacks')
@@ -0,0 +1,31 @@
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)
23
+ raise ArgumentError, "doc.database required to delete_attachment" unless database
24
+ result = database.delete_attachment(self, name)
25
+ self['_rev'] = result['rev']
26
+ result['ok']
27
+ end
28
+
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,483 @@
1
+ require File.join(File.dirname(__FILE__), '..', 'support', 'class')
2
+
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
6
+
7
+ module CouchRest
8
+ # Callbacks are hooks into the lifecycle of an object that allow you to trigger logic
9
+ # before or after an alteration of the object state.
10
+ #
11
+ # Mixing in this module allows you to define callbacks in your class.
12
+ #
13
+ # Example:
14
+ # class Storage
15
+ # include ActiveSupport::Callbacks
16
+ #
17
+ # define_callbacks :save
18
+ # end
19
+ #
20
+ # class ConfigStorage < Storage
21
+ # save_callback :before, :saving_message
22
+ # def saving_message
23
+ # puts "saving..."
24
+ # end
25
+ #
26
+ # save_callback :after do |object|
27
+ # puts "saved"
28
+ # end
29
+ #
30
+ # def save
31
+ # _run_save_callbacks do
32
+ # puts "- save"
33
+ # end
34
+ # end
35
+ # end
36
+ #
37
+ # config = ConfigStorage.new
38
+ # config.save
39
+ #
40
+ # Output:
41
+ # saving...
42
+ # - save
43
+ # saved
44
+ #
45
+ # Callbacks from parent classes are inherited.
46
+ #
47
+ # Example:
48
+ # class Storage
49
+ # include ActiveSupport::Callbacks
50
+ #
51
+ # define_callbacks :save
52
+ #
53
+ # save_callback :before, :prepare
54
+ # def prepare
55
+ # puts "preparing save"
56
+ # end
57
+ # end
58
+ #
59
+ # class ConfigStorage < Storage
60
+ # save_callback :before, :saving_message
61
+ # def saving_message
62
+ # puts "saving..."
63
+ # end
64
+ #
65
+ # save_callback :after do |object|
66
+ # puts "saved"
67
+ # end
68
+ #
69
+ # def save
70
+ # _run_save_callbacks do
71
+ # puts "- save"
72
+ # end
73
+ # end
74
+ # end
75
+ #
76
+ # config = ConfigStorage.new
77
+ # config.save
78
+ #
79
+ # Output:
80
+ # preparing save
81
+ # saving...
82
+ # - save
83
+ # saved
84
+ module Callbacks
85
+ def self.included(klass)
86
+ klass.extend ClassMethods
87
+ end
88
+
89
+ def run_callbacks(kind, options = {}, &blk)
90
+ send("_run_#{kind}_callbacks", &blk)
91
+ end
92
+
93
+ class Callback
94
+ @@_callback_sequence = 0
95
+
96
+ attr_accessor :filter, :kind, :name, :options, :per_key, :klass
97
+ def initialize(filter, kind, options, klass, name)
98
+ @kind, @klass = kind, klass
99
+ @name = name
100
+
101
+ normalize_options!(options)
102
+
103
+ @per_key = options.delete(:per_key)
104
+ @raw_filter, @options = filter, options
105
+ @filter = _compile_filter(filter)
106
+ @compiled_options = _compile_options(options)
107
+ @callback_id = next_id
108
+
109
+ _compile_per_key_options
110
+ end
111
+
112
+ def clone(klass)
113
+ obj = super()
114
+ obj.klass = klass
115
+ obj.per_key = @per_key.dup
116
+ obj.options = @options.dup
117
+ obj.per_key[:if] = @per_key[:if].dup
118
+ obj.per_key[:unless] = @per_key[:unless].dup
119
+ obj.options[:if] = @options[:if].dup
120
+ obj.options[:unless] = @options[:unless].dup
121
+ obj
122
+ end
123
+
124
+ def normalize_options!(options)
125
+ options[:if] = Array(options[:if])
126
+ options[:unless] = Array(options[:unless])
127
+
128
+ options[:per_key] ||= {}
129
+ options[:per_key][:if] = Array(options[:per_key][:if])
130
+ options[:per_key][:unless] = Array(options[:per_key][:unless])
131
+ end
132
+
133
+ def next_id
134
+ @@_callback_sequence += 1
135
+ end
136
+
137
+ def matches?(_kind, _name, _filter)
138
+ @kind == _kind &&
139
+ @name == _name &&
140
+ @filter == _filter
141
+ end
142
+
143
+ def _update_filter(filter_options, new_options)
144
+ filter_options[:if].push(new_options[:unless]) if new_options.key?(:unless)
145
+ filter_options[:unless].push(new_options[:if]) if new_options.key?(:if)
146
+ end
147
+
148
+ def recompile!(_options, _per_key)
149
+ _update_filter(self.options, _options)
150
+ _update_filter(self.per_key, _per_key)
151
+
152
+ @callback_id = next_id
153
+ @filter = _compile_filter(@raw_filter)
154
+ @compiled_options = _compile_options(@options)
155
+ _compile_per_key_options
156
+ end
157
+
158
+ def _compile_per_key_options
159
+ key_options = _compile_options(@per_key)
160
+
161
+ @klass.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
162
+ def _one_time_conditions_valid_#{@callback_id}?
163
+ true #{key_options[0]}
164
+ end
165
+ RUBY_EVAL
166
+ end
167
+
168
+ # This will supply contents for before and around filters, and no
169
+ # contents for after filters (for the forward pass).
170
+ def start(key = nil, options = {})
171
+ object, terminator = (options || {}).values_at(:object, :terminator)
172
+
173
+ return if key && !object.send("_one_time_conditions_valid_#{@callback_id}?")
174
+
175
+ terminator ||= false
176
+
177
+ # options[0] is the compiled form of supplied conditions
178
+ # options[1] is the "end" for the conditional
179
+
180
+ if @kind == :before || @kind == :around
181
+ if @kind == :before
182
+ # if condition # before_save :filter_name, :if => :condition
183
+ # filter_name
184
+ # end
185
+ filter = <<-RUBY_EVAL
186
+ unless halted
187
+ result = #{@filter}
188
+ halted ||= (#{terminator})
189
+ end
190
+ RUBY_EVAL
191
+ [@compiled_options[0], filter, @compiled_options[1]].compact.join("\n")
192
+ else
193
+ # Compile around filters with conditions into proxy methods
194
+ # that contain the conditions.
195
+ #
196
+ # For `around_save :filter_name, :if => :condition':
197
+ #
198
+ # def _conditional_callback_save_17
199
+ # if condition
200
+ # filter_name do
201
+ # yield self
202
+ # end
203
+ # else
204
+ # yield self
205
+ # end
206
+ # end
207
+
208
+ name = "_conditional_callback_#{@kind}_#{next_id}"
209
+ txt = <<-RUBY_EVAL
210
+ def #{name}(halted)
211
+ #{@compiled_options[0] || "if true"} && !halted
212
+ #{@filter} do
213
+ yield self
214
+ end
215
+ else
216
+ yield self
217
+ end
218
+ end
219
+ RUBY_EVAL
220
+ @klass.class_eval(txt)
221
+ "#{name}(halted) do"
222
+ end
223
+ end
224
+ end
225
+
226
+ # This will supply contents for around and after filters, but not
227
+ # before filters (for the backward pass).
228
+ def end(key = nil, options = {})
229
+ object = (options || {})[:object]
230
+
231
+ return if key && !object.send("_one_time_conditions_valid_#{@callback_id}?")
232
+
233
+ if @kind == :around || @kind == :after
234
+ # if condition # after_save :filter_name, :if => :condition
235
+ # filter_name
236
+ # end
237
+ if @kind == :after
238
+ [@compiled_options[0], @filter, @compiled_options[1]].compact.join("\n")
239
+ else
240
+ "end"
241
+ end
242
+ end
243
+ end
244
+
245
+ private
246
+ # Options support the same options as filters themselves (and support
247
+ # symbols, string, procs, and objects), so compile a conditional
248
+ # expression based on the options
249
+ def _compile_options(options)
250
+ return [] if options[:if].empty? && options[:unless].empty?
251
+
252
+ conditions = []
253
+
254
+ unless options[:if].empty?
255
+ conditions << Array(_compile_filter(options[:if]))
256
+ end
257
+
258
+ unless options[:unless].empty?
259
+ conditions << Array(_compile_filter(options[:unless])).map {|f| "!#{f}"}
260
+ end
261
+
262
+ ["if #{conditions.flatten.join(" && ")}", "end"]
263
+ end
264
+
265
+ # Filters support:
266
+ # Arrays:: Used in conditions. This is used to specify
267
+ # multiple conditions. Used internally to
268
+ # merge conditions from skip_* filters
269
+ # Symbols:: A method to call
270
+ # Strings:: Some content to evaluate
271
+ # Procs:: A proc to call with the object
272
+ # Objects:: An object with a before_foo method on it to call
273
+ #
274
+ # All of these objects are compiled into methods and handled
275
+ # the same after this point:
276
+ # Arrays:: Merged together into a single filter
277
+ # Symbols:: Already methods
278
+ # Strings:: class_eval'ed into methods
279
+ # Procs:: define_method'ed into methods
280
+ # Objects::
281
+ # a method is created that calls the before_foo method
282
+ # on the object.
283
+ def _compile_filter(filter)
284
+ method_name = "_callback_#{@kind}_#{next_id}"
285
+ case filter
286
+ when Array
287
+ filter.map {|f| _compile_filter(f)}
288
+ when Symbol
289
+ filter
290
+ when Proc
291
+ @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}
297
+ end
298
+ 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
305
+ method_name
306
+ end
307
+ end
308
+ end
309
+
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)
315
+ end
316
+ super
317
+ end
318
+
319
+ # An Array with a compile method
320
+ class CallbackChain < Array
321
+ def initialize(symbol)
322
+ @symbol = symbol
323
+ end
324
+
325
+ def compile(key = nil, options = {})
326
+ method = []
327
+ method << "halted = false"
328
+ each do |callback|
329
+ method << callback.start(key, options)
330
+ end
331
+ method << "yield self if block_given?"
332
+ reverse_each do |callback|
333
+ method << callback.end(key, options)
334
+ end
335
+ method.compact.join("\n")
336
+ end
337
+
338
+ def clone(klass)
339
+ chain = CallbackChain.new(@symbol)
340
+ chain.push(*map {|c| c.clone(klass)})
341
+ end
342
+ end
343
+
344
+ module ClassMethods
345
+ CHAINS = {:before => :before, :around => :before, :after => :after} unless self.const_defined?("CHAINS")
346
+
347
+ # Make the _run_save_callbacks method. The generated method takes
348
+ # a block that it'll yield to. It'll call the before and around filters
349
+ # in order, yield the block, and then run the after filters.
350
+ #
351
+ # _run_save_callbacks do
352
+ # save
353
+ # end
354
+ #
355
+ # The _run_save_callbacks method can optionally take a key, which
356
+ # will be used to compile an optimized callback method for each
357
+ # 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)
361
+ if key
362
+ send("_run__\#{self.class.name.split("::").last}__#{symbol}__\#{key}__callbacks") { yield if block_given? }
363
+ else
364
+ #{str}
365
+ end
366
+ end
367
+ 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)
373
+ end
374
+
375
+ # This is called the first time a callback is called with a particular
376
+ # key. It creates a new callback method for the key, calculating
377
+ # which callbacks can be omitted because of per_key conditions.
378
+ def _create_and_run_keyed_callback(klass, kind, key, obj, &blk)
379
+ @_keyed_callbacks ||= {}
380
+ @_keyed_callbacks[[kind, key]] ||= begin
381
+ str = self.send("_#{kind}_callbacks").compile(key, :object => obj, :terminator => self.send("_#{kind}_terminator"))
382
+
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
+ true
390
+ end
391
+
392
+ obj.send("_run__#{klass.split("::").last}__#{kind}__#{key}__callbacks", &blk)
393
+ end
394
+
395
+ # Define callbacks.
396
+ #
397
+ # Creates a <name>_callback method that you can use to add callbacks.
398
+ #
399
+ # Syntax:
400
+ # save_callback :before, :before_meth
401
+ # save_callback :after, :after_meth, :if => :condition
402
+ # save_callback :around {|r| stuff; yield; stuff }
403
+ #
404
+ # The <name>_callback method also updates the _run_<name>_callbacks
405
+ # method, which is the public API to run the callbacks.
406
+ #
407
+ # Also creates a skip_<name>_callback method that you can use to skip
408
+ # callbacks.
409
+ #
410
+ # When creating or skipping callbacks, you can specify conditions that
411
+ # are always the same for a given key. For instance, in ActionPack,
412
+ # we convert :only and :except conditions into per-key conditions.
413
+ #
414
+ # before_filter :authenticate, :except => "index"
415
+ # becomes
416
+ # dispatch_callback :before, :authenticate, :per_key => {:unless => proc {|c| c.action_name == "index"}}
417
+ #
418
+ # Per-Key conditions are evaluated only once per use of a given key.
419
+ # In the case of the above example, you would do:
420
+ #
421
+ # run_dispatch_callbacks(action_name) { ... dispatch stuff ... }
422
+ #
423
+ # In that case, each action_name would get its own compiled callback
424
+ # method that took into consideration the per_key conditions. This
425
+ # is a speed improvement for ActionPack.
426
+ def define_callbacks(*symbols)
427
+ terminator = symbols.pop if symbols.last.is_a?(String)
428
+ 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)
464
+ end
465
+ _define_runner(:#{symbol},
466
+ self._#{symbol}_callbacks.compile(nil, :terminator => _#{symbol}_terminator),
467
+ options)
468
+ 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
479
+ end
480
+ end
481
+ end
482
+ end
483
+ end