scottraio-couchrest 0.2

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 (89) hide show
  1. data/LICENSE +176 -0
  2. data/README.md +93 -0
  3. data/Rakefile +66 -0
  4. data/THANKS.md +18 -0
  5. data/examples/model/example.rb +144 -0
  6. data/examples/word_count/markov +38 -0
  7. data/examples/word_count/views/books/chunked-map.js +3 -0
  8. data/examples/word_count/views/books/united-map.js +1 -0
  9. data/examples/word_count/views/markov/chain-map.js +6 -0
  10. data/examples/word_count/views/markov/chain-reduce.js +7 -0
  11. data/examples/word_count/views/word_count/count-map.js +6 -0
  12. data/examples/word_count/views/word_count/count-reduce.js +3 -0
  13. data/examples/word_count/word_count.rb +46 -0
  14. data/examples/word_count/word_count_query.rb +40 -0
  15. data/examples/word_count/word_count_views.rb +26 -0
  16. data/lib/couchrest.rb +189 -0
  17. data/lib/couchrest/commands/generate.rb +71 -0
  18. data/lib/couchrest/commands/push.rb +103 -0
  19. data/lib/couchrest/core/database.rb +313 -0
  20. data/lib/couchrest/core/design.rb +89 -0
  21. data/lib/couchrest/core/document.rb +96 -0
  22. data/lib/couchrest/core/response.rb +16 -0
  23. data/lib/couchrest/core/server.rb +88 -0
  24. data/lib/couchrest/core/view.rb +4 -0
  25. data/lib/couchrest/helper/pager.rb +103 -0
  26. data/lib/couchrest/helper/streamer.rb +44 -0
  27. data/lib/couchrest/mixins.rb +4 -0
  28. data/lib/couchrest/mixins/attachments.rb +31 -0
  29. data/lib/couchrest/mixins/callbacks.rb +442 -0
  30. data/lib/couchrest/mixins/design_doc.rb +63 -0
  31. data/lib/couchrest/mixins/document_queries.rb +48 -0
  32. data/lib/couchrest/mixins/extended_attachments.rb +68 -0
  33. data/lib/couchrest/mixins/extended_document_mixins.rb +6 -0
  34. data/lib/couchrest/mixins/properties.rb +125 -0
  35. data/lib/couchrest/mixins/validation.rb +234 -0
  36. data/lib/couchrest/mixins/views.rb +169 -0
  37. data/lib/couchrest/monkeypatches.rb +113 -0
  38. data/lib/couchrest/more/casted_model.rb +28 -0
  39. data/lib/couchrest/more/extended_document.rb +217 -0
  40. data/lib/couchrest/more/property.rb +40 -0
  41. data/lib/couchrest/support/blank.rb +42 -0
  42. data/lib/couchrest/support/class.rb +175 -0
  43. data/lib/couchrest/validation/auto_validate.rb +163 -0
  44. data/lib/couchrest/validation/contextual_validators.rb +78 -0
  45. data/lib/couchrest/validation/validation_errors.rb +118 -0
  46. data/lib/couchrest/validation/validators/absent_field_validator.rb +74 -0
  47. data/lib/couchrest/validation/validators/confirmation_validator.rb +99 -0
  48. data/lib/couchrest/validation/validators/format_validator.rb +117 -0
  49. data/lib/couchrest/validation/validators/formats/email.rb +66 -0
  50. data/lib/couchrest/validation/validators/formats/url.rb +43 -0
  51. data/lib/couchrest/validation/validators/generic_validator.rb +120 -0
  52. data/lib/couchrest/validation/validators/length_validator.rb +134 -0
  53. data/lib/couchrest/validation/validators/method_validator.rb +89 -0
  54. data/lib/couchrest/validation/validators/numeric_validator.rb +104 -0
  55. data/lib/couchrest/validation/validators/required_field_validator.rb +109 -0
  56. data/spec/couchrest/core/couchrest_spec.rb +201 -0
  57. data/spec/couchrest/core/database_spec.rb +745 -0
  58. data/spec/couchrest/core/design_spec.rb +131 -0
  59. data/spec/couchrest/core/document_spec.rb +311 -0
  60. data/spec/couchrest/core/server_spec.rb +35 -0
  61. data/spec/couchrest/helpers/pager_spec.rb +122 -0
  62. data/spec/couchrest/helpers/streamer_spec.rb +23 -0
  63. data/spec/couchrest/more/casted_extended_doc_spec.rb +40 -0
  64. data/spec/couchrest/more/casted_model_spec.rb +97 -0
  65. data/spec/couchrest/more/extended_doc_attachment_spec.rb +129 -0
  66. data/spec/couchrest/more/extended_doc_spec.rb +509 -0
  67. data/spec/couchrest/more/extended_doc_view_spec.rb +207 -0
  68. data/spec/couchrest/more/property_spec.rb +129 -0
  69. data/spec/fixtures/attachments/README +3 -0
  70. data/spec/fixtures/attachments/couchdb.png +0 -0
  71. data/spec/fixtures/attachments/test.html +11 -0
  72. data/spec/fixtures/more/article.rb +34 -0
  73. data/spec/fixtures/more/card.rb +20 -0
  74. data/spec/fixtures/more/course.rb +14 -0
  75. data/spec/fixtures/more/event.rb +6 -0
  76. data/spec/fixtures/more/invoice.rb +17 -0
  77. data/spec/fixtures/more/person.rb +8 -0
  78. data/spec/fixtures/more/question.rb +6 -0
  79. data/spec/fixtures/more/service.rb +12 -0
  80. data/spec/fixtures/views/lib.js +3 -0
  81. data/spec/fixtures/views/test_view/lib.js +3 -0
  82. data/spec/fixtures/views/test_view/only-map.js +4 -0
  83. data/spec/fixtures/views/test_view/test-map.js +3 -0
  84. data/spec/fixtures/views/test_view/test-reduce.js +3 -0
  85. data/spec/spec.opts +6 -0
  86. data/spec/spec_helper.rb +26 -0
  87. data/utils/remap.rb +27 -0
  88. data/utils/subset.rb +30 -0
  89. metadata +217 -0
@@ -0,0 +1,96 @@
1
+ require 'delegate'
2
+
3
+ module CouchRest
4
+ class Document < Response
5
+ include CouchRest::Mixins::Attachments
6
+
7
+ # def self.inherited(subklass)
8
+ # subklass.send(:class_inheritable_accessor, :database)
9
+ # end
10
+
11
+ class_inheritable_accessor :database
12
+ attr_accessor :database
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
+ def id
22
+ self['_id']
23
+ end
24
+
25
+ def rev
26
+ self['_rev']
27
+ end
28
+
29
+ # returns true if the document has never been saved
30
+ def new_document?
31
+ !rev
32
+ end
33
+
34
+ # Saves the document to the db using create or update. Also runs the :save
35
+ # callbacks. Sets the <tt>_id</tt> and <tt>_rev</tt> fields based on
36
+ # CouchDB's response.
37
+ # If <tt>bulk</tt> is <tt>true</tt> (defaults to false) the document is cached for bulk save.
38
+ def save(bulk = false)
39
+ raise ArgumentError, "doc.database required for saving" unless database
40
+ result = database.save_doc self, bulk
41
+ result['ok']
42
+ end
43
+
44
+ # Deletes the document from the database. Runs the :delete callbacks.
45
+ # Removes the <tt>_id</tt> and <tt>_rev</tt> fields, preparing the
46
+ # document to be saved to a new <tt>_id</tt>.
47
+ # If <tt>bulk</tt> is <tt>true</tt> (defaults to false) the document won't
48
+ # actually be deleted from the db until bulk save.
49
+ def destroy(bulk = false)
50
+ raise ArgumentError, "doc.database required to destroy" unless database
51
+ result = database.delete_doc(self, bulk)
52
+ if result['ok']
53
+ self['_rev'] = nil
54
+ self['_id'] = nil
55
+ end
56
+ result['ok']
57
+ end
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
62
+ def copy(dest)
63
+ raise ArgumentError, "doc.database required to copy" unless database
64
+ result = database.copy_doc(self, dest)
65
+ result['ok']
66
+ end
67
+
68
+ # moves the document to a new id. If the destination id currently exists, a rev must be provided.
69
+ # <tt>dest</tt> can take one of two forms if overwriting: "id_to_overwrite?rev=revision" or the actual doc
70
+ # hash with a '_rev' key
71
+ def move(dest)
72
+ raise ArgumentError, "doc.database required to copy" unless database
73
+ result = database.move_doc(self, dest)
74
+ result['ok']
75
+ end
76
+
77
+ # Returns the CouchDB uri for the document
78
+ def uri(append_rev = false)
79
+ return nil if new_document?
80
+ couch_uri = "http://#{database.uri}/#{CGI.escape(id)}"
81
+ if append_rev == true
82
+ couch_uri << "?rev=#{rev}"
83
+ elsif append_rev.kind_of?(Integer)
84
+ couch_uri << "?rev=#{append_rev}"
85
+ end
86
+ couch_uri
87
+ end
88
+
89
+ # Returns the document's database
90
+ def database
91
+ @database || self.class.database
92
+ end
93
+
94
+ end
95
+
96
+ 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
@@ -0,0 +1,88 @@
1
+ module CouchRest
2
+ class Server
3
+ attr_accessor :uri, :uuid_batch_count, :available_databases
4
+ def initialize(server = 'http://127.0.0.1:5984', uuid_batch_count = 1000)
5
+ @uri = server
6
+ @uuid_batch_count = uuid_batch_count
7
+ end
8
+
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
47
+ def databases
48
+ CouchRest.get "#{@uri}/_all_dbs"
49
+ end
50
+
51
+ # Returns a CouchRest::Database for the given name
52
+ def database(name)
53
+ CouchRest::Database.new(self, name)
54
+ end
55
+
56
+ # Creates the database if it doesn't exist
57
+ def database!(name)
58
+ create_db(name) rescue nil
59
+ database(name)
60
+ end
61
+
62
+ # GET the welcome message
63
+ def info
64
+ CouchRest.get "#{@uri}/"
65
+ end
66
+
67
+ # Create a database
68
+ def create_db(name)
69
+ CouchRest.put "#{@uri}/#{name}"
70
+ database(name)
71
+ end
72
+
73
+ # Restart the CouchDB instance
74
+ def restart!
75
+ CouchRest.post "#{@uri}/_restart"
76
+ end
77
+
78
+ # Retrive an unused UUID from CouchDB. Server instances manage caching a list of unused UUIDs.
79
+ def next_uuid(count = @uuid_batch_count)
80
+ @uuids ||= []
81
+ if @uuids.empty?
82
+ @uuids = CouchRest.get("#{@uri}/_uuids?count=#{count}")["uuids"]
83
+ end
84
+ @uuids.pop
85
+ end
86
+
87
+ end
88
+ end
@@ -0,0 +1,4 @@
1
+ module CouchRest
2
+ class View
3
+ end
4
+ end
@@ -0,0 +1,103 @@
1
+ module CouchRest
2
+ class Pager
3
+ attr_accessor :db
4
+ def initialize db
5
+ @db = db
6
+ end
7
+
8
+ def all_docs(limit=100, &block)
9
+ startkey = nil
10
+ oldend = nil
11
+
12
+ while docrows = request_all_docs(limit+1, startkey)
13
+ startkey = docrows.last['key']
14
+ docrows.pop if docrows.length > limit
15
+ if oldend == startkey
16
+ break
17
+ end
18
+ yield(docrows)
19
+ oldend = startkey
20
+ end
21
+ end
22
+
23
+ def key_reduce(view, limit=2000, firstkey = nil, lastkey = nil, &block)
24
+ # start with no keys
25
+ startkey = firstkey
26
+ # lastprocessedkey = nil
27
+ keepgoing = true
28
+
29
+ while keepgoing && viewrows = request_view(view, limit, startkey)
30
+ startkey = viewrows.first['key']
31
+ endkey = viewrows.last['key']
32
+
33
+ if (startkey == endkey)
34
+ # we need to rerequest to get a bigger page
35
+ # so we know we have all the rows for that key
36
+ viewrows = @db.view(view, :key => startkey)['rows']
37
+ # we need to do an offset thing to find the next startkey
38
+ # otherwise we just get stuck
39
+ lastdocid = viewrows.last['id']
40
+ fornextloop = @db.view(view, :startkey => startkey, :startkey_docid => lastdocid, :limit => 2)['rows']
41
+
42
+ newendkey = fornextloop.last['key']
43
+ if (newendkey == endkey)
44
+ keepgoing = false
45
+ else
46
+ startkey = newendkey
47
+ end
48
+ rows = viewrows
49
+ else
50
+ rows = []
51
+ for r in viewrows
52
+ if (lastkey && r['key'] == lastkey)
53
+ keepgoing = false
54
+ break
55
+ end
56
+ break if (r['key'] == endkey)
57
+ rows << r
58
+ end
59
+ startkey = endkey
60
+ end
61
+
62
+ key = :begin
63
+ values = []
64
+
65
+ rows.each do |r|
66
+ if key != r['key']
67
+ # we're on a new key, yield the old first and then reset
68
+ yield(key, values) if key != :begin
69
+ key = r['key']
70
+ values = []
71
+ end
72
+ # keep accumulating
73
+ values << r['value']
74
+ end
75
+ yield(key, values)
76
+
77
+ end
78
+ end
79
+
80
+ private
81
+
82
+ def request_all_docs limit, startkey = nil
83
+ opts = {}
84
+ opts[:limit] = limit if limit
85
+ opts[:startkey] = startkey if startkey
86
+ results = @db.documents(opts)
87
+ rows = results['rows']
88
+ rows unless rows.length == 0
89
+ end
90
+
91
+ def request_view view, limit = nil, startkey = nil, endkey = nil
92
+ opts = {}
93
+ opts[:limit] = limit if limit
94
+ opts[:startkey] = startkey if startkey
95
+ opts[:endkey] = endkey if endkey
96
+
97
+ results = @db.view(view, opts)
98
+ rows = results['rows']
99
+ rows unless rows.length == 0
100
+ end
101
+
102
+ end
103
+ end
@@ -0,0 +1,44 @@
1
+ module CouchRest
2
+ class Streamer
3
+ attr_accessor :db
4
+ def initialize db
5
+ @db = db
6
+ end
7
+
8
+ # Stream a view, yielding one row at a time. Shells out to <tt>curl</tt> to keep RAM usage low when you have millions of rows.
9
+ def view name, params = nil, &block
10
+ urlst = /^_/.match(name) ? "#{@db.root}/#{name}" : "#{@db.root}/_view/#{name}"
11
+ url = CouchRest.paramify_url urlst, params
12
+ # puts "stream #{url}"
13
+ first = nil
14
+ IO.popen("curl --silent #{url}") do |view|
15
+ first = view.gets # discard header
16
+ while line = view.gets
17
+ row = parse_line(line)
18
+ block.call row
19
+ end
20
+ end
21
+ parse_first(first)
22
+ end
23
+
24
+ private
25
+
26
+ def parse_line line
27
+ return nil unless line
28
+ if /(\{.*\}),?/.match(line.chomp)
29
+ JSON.parse($1)
30
+ end
31
+ end
32
+
33
+ def parse_first first
34
+ return nil unless first
35
+ parts = first.split(',')
36
+ parts.pop
37
+ line = parts.join(',')
38
+ JSON.parse("#{line}}")
39
+ rescue
40
+ nil
41
+ end
42
+
43
+ end
44
+ 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,442 @@
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/18b405f154868204a8f332888871041a7bad95e1/activesupport/lib/active_support/callbacks.rb
5
+
6
+ module CouchRest
7
+ # Callbacks are hooks into the lifecycle of an object that allow you to trigger logic
8
+ # before or after an alteration of the object state.
9
+ #
10
+ # Mixing in this module allows you to define callbacks in your class.
11
+ #
12
+ # Example:
13
+ # class Storage
14
+ # include CouchRest::Callbacks
15
+ #
16
+ # define_callbacks :save
17
+ # end
18
+ #
19
+ # class ConfigStorage < Storage
20
+ # save_callback :before, :saving_message
21
+ # def saving_message
22
+ # puts "saving..."
23
+ # end
24
+ #
25
+ # save_callback :after do |object|
26
+ # puts "saved"
27
+ # end
28
+ #
29
+ # def save
30
+ # _run_save_callbacks do
31
+ # puts "- save"
32
+ # end
33
+ # end
34
+ # end
35
+ #
36
+ # config = ConfigStorage.new
37
+ # config.save
38
+ #
39
+ # Output:
40
+ # saving...
41
+ # - save
42
+ # saved
43
+ #
44
+ # Callbacks from parent classes are inherited.
45
+ #
46
+ # Example:
47
+ # class Storage
48
+ # include CouchRest::Callbacks
49
+ #
50
+ # define_callbacks :save
51
+ #
52
+ # save_callback :before, :prepare
53
+ # def prepare
54
+ # puts "preparing save"
55
+ # end
56
+ # end
57
+ #
58
+ # class ConfigStorage < Storage
59
+ # save_callback :before, :saving_message
60
+ # def saving_message
61
+ # puts "saving..."
62
+ # end
63
+ #
64
+ # save_callback :after do |object|
65
+ # puts "saved"
66
+ # end
67
+ #
68
+ # def save
69
+ # _run_save_callbacks do
70
+ # puts "- save"
71
+ # end
72
+ # end
73
+ # end
74
+ #
75
+ # config = ConfigStorage.new
76
+ # config.save
77
+ #
78
+ # Output:
79
+ # preparing save
80
+ # saving...
81
+ # - save
82
+ # saved
83
+ module Callbacks
84
+ def self.included(klass)
85
+ klass.extend ClassMethods
86
+ end
87
+
88
+ def run_callbacks(kind, options = {})
89
+ send("_run_#{kind}_callbacks")
90
+ end
91
+
92
+ class Callback
93
+ @@_callback_sequence = 0
94
+
95
+ attr_accessor :filter, :kind, :name, :options, :per_key, :klass
96
+ def initialize(filter, kind, options, klass, name)
97
+ @kind, @klass = kind, klass
98
+ @name = name
99
+
100
+ normalize_options!(options)
101
+
102
+ @per_key = options.delete(:per_key)
103
+ @raw_filter, @options = filter, options
104
+ @filter = _compile_filter(filter)
105
+ @compiled_options = _compile_options(options)
106
+ @callback_id = next_id
107
+
108
+ _compile_per_key_options
109
+ end
110
+
111
+ def clone(klass)
112
+ obj = super()
113
+ obj.klass = klass
114
+ obj.per_key = @per_key.dup
115
+ obj.options = @options.dup
116
+ obj.per_key[:if] = @per_key[:if].dup
117
+ obj.per_key[:unless] = @per_key[:unless].dup
118
+ obj.options[:if] = @options[:if].dup
119
+ obj.options[:unless] = @options[:unless].dup
120
+ obj
121
+ end
122
+
123
+ def normalize_options!(options)
124
+ options[:if] = Array(options[:if])
125
+ options[:unless] = Array(options[:unless])
126
+
127
+ options[:per_key] ||= {}
128
+ options[:per_key][:if] = Array(options[:per_key][:if])
129
+ options[:per_key][:unless] = Array(options[:per_key][:unless])
130
+ end
131
+
132
+ def next_id
133
+ @@_callback_sequence += 1
134
+ end
135
+
136
+ def matches?(_kind, _name, _filter)
137
+ @kind == _kind &&
138
+ @name == _name &&
139
+ @filter == _filter
140
+ end
141
+
142
+ def _update_filter(filter_options, new_options)
143
+ filter_options[:if].push(new_options[:unless]) if new_options.key?(:unless)
144
+ filter_options[:unless].push(new_options[:if]) if new_options.key?(:if)
145
+ end
146
+
147
+ def recompile!(_options, _per_key)
148
+ _update_filter(self.options, _options)
149
+ _update_filter(self.per_key, _per_key)
150
+
151
+ @callback_id = next_id
152
+ @filter = _compile_filter(@raw_filter)
153
+ @compiled_options = _compile_options(@options)
154
+ _compile_per_key_options
155
+ end
156
+
157
+ def _compile_per_key_options
158
+ key_options = _compile_options(@per_key)
159
+
160
+ @klass.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
161
+ def _one_time_conditions_valid_#{@callback_id}?
162
+ true #{key_options[0]}
163
+ end
164
+ RUBY_EVAL
165
+ end
166
+
167
+ # This will supply contents for before and around filters, and no
168
+ # contents for after filters (for the forward pass).
169
+ def start(key = nil, object = nil)
170
+ return if key && !object.send("_one_time_conditions_valid_#{@callback_id}?")
171
+
172
+ # options[0] is the compiled form of supplied conditions
173
+ # options[1] is the "end" for the conditional
174
+
175
+ if @kind == :before || @kind == :around
176
+ if @kind == :before
177
+ # if condition # before_save :filter_name, :if => :condition
178
+ # filter_name
179
+ # end
180
+ [@compiled_options[0], @filter, @compiled_options[1]].compact.join("\n")
181
+ elsif @compiled_options[0]
182
+ # Compile around filters with conditions into proxy methods
183
+ # that contain the conditions.
184
+ #
185
+ # For `around_save :filter_name, :if => :condition':
186
+ #
187
+ # def _conditional_callback_save_17
188
+ # if condition
189
+ # filter_name do
190
+ # yield self
191
+ # end
192
+ # else
193
+ # yield self
194
+ # end
195
+ # end
196
+
197
+ name = "_conditional_callback_#{@kind}_#{next_id}"
198
+ txt = <<-RUBY_EVAL
199
+ def #{name}
200
+ #{@compiled_options[0]}
201
+ #{@filter} do
202
+ yield self
203
+ end
204
+ else
205
+ yield self
206
+ end
207
+ end
208
+ RUBY_EVAL
209
+ @klass.class_eval(txt)
210
+ "#{name} do"
211
+ else
212
+ "#{@filter} do"
213
+ end
214
+ end
215
+ end
216
+
217
+ # This will supply contents for around and after filters, but not
218
+ # before filters (for the backward pass).
219
+ def end(key = nil, object = nil)
220
+ return if key && !object.send("_one_time_conditions_valid_#{@callback_id}?")
221
+
222
+ if @kind == :around || @kind == :after
223
+ # if condition # after_save :filter_name, :if => :condition
224
+ # filter_name
225
+ # end
226
+ if @kind == :after
227
+ [@compiled_options[0], @filter, @compiled_options[1]].compact.join("\n")
228
+ else
229
+ "end"
230
+ end
231
+ end
232
+ end
233
+
234
+ private
235
+ # Options support the same options as filters themselves (and support
236
+ # symbols, string, procs, and objects), so compile a conditional
237
+ # expression based on the options
238
+ def _compile_options(options)
239
+ return [] if options[:if].empty? && options[:unless].empty?
240
+
241
+ conditions = []
242
+
243
+ unless options[:if].empty?
244
+ conditions << Array(_compile_filter(options[:if]))
245
+ end
246
+
247
+ unless options[:unless].empty?
248
+ conditions << Array(_compile_filter(options[:unless])).map {|f| "!#{f}"}
249
+ end
250
+
251
+ ["if #{conditions.flatten.join(" && ")}", "end"]
252
+ end
253
+
254
+ # Filters support:
255
+ # Arrays:: Used in conditions. This is used to specify
256
+ # multiple conditions. Used internally to
257
+ # merge conditions from skip_* filters
258
+ # Symbols:: A method to call
259
+ # Strings:: Some content to evaluate
260
+ # Procs:: A proc to call with the object
261
+ # Objects:: An object with a before_foo method on it to call
262
+ #
263
+ # All of these objects are compiled into methods and handled
264
+ # the same after this point:
265
+ # Arrays:: Merged together into a single filter
266
+ # Symbols:: Already methods
267
+ # Strings:: class_eval'ed into methods
268
+ # Procs:: define_method'ed into methods
269
+ # Objects::
270
+ # a method is created that calls the before_foo method
271
+ # on the object.
272
+ def _compile_filter(filter)
273
+ method_name = "_callback_#{@kind}_#{next_id}"
274
+ case filter
275
+ when Array
276
+ filter.map {|f| _compile_filter(f)}
277
+ when Symbol
278
+ filter
279
+ when Proc
280
+ @klass.send(:define_method, method_name, &filter)
281
+ method_name << (filter.arity == 1 ? "(self)" : "")
282
+ when String
283
+ @klass.class_eval <<-RUBY_EVAL
284
+ def #{method_name}
285
+ #{filter}
286
+ end
287
+ RUBY_EVAL
288
+ method_name
289
+ else
290
+ kind, name = @kind, @name
291
+ @klass.send(:define_method, method_name) do
292
+ filter.send("#{kind}_#{name}", self)
293
+ end
294
+ method_name
295
+ end
296
+ end
297
+ end
298
+
299
+ # This method_missing is supplied to catch callbacks with keys and create
300
+ # the appropriate callback for future use.
301
+ def method_missing(meth, *args, &blk)
302
+ if meth.to_s =~ /_run_(\w+)_(\w+)_(\w+)_callbacks/
303
+ return self.class._create_and_run_keyed_callback($1, $2.to_sym, $3.to_sym, self, &blk)
304
+ end
305
+ super
306
+ end
307
+
308
+ # An Array with a compile method
309
+ class CallbackChain < Array
310
+ def compile(key = nil, object = nil)
311
+ method = []
312
+ each do |callback|
313
+ method << callback.start(key, object)
314
+ end
315
+ method << "yield self"
316
+ reverse_each do |callback|
317
+ method << callback.end(key, object)
318
+ end
319
+ method.compact.join("\n")
320
+ end
321
+
322
+ def clone(klass)
323
+ CallbackChain.new(map {|c| c.clone(klass)})
324
+ end
325
+ end
326
+
327
+ module ClassMethods
328
+ CHAINS = {:before => :before, :around => :before, :after => :after} unless self.const_defined?("CHAINS")
329
+
330
+ # Make the _run_save_callbacks method. The generated method takes
331
+ # a block that it'll yield to. It'll call the before and around filters
332
+ # in order, yield the block, and then run the after filters.
333
+ #
334
+ # _run_save_callbacks do
335
+ # save
336
+ # end
337
+ #
338
+ # The _run_save_callbacks method can optionally take a key, which
339
+ # will be used to compile an optimized callback method for each
340
+ # key. See #define_callbacks for more information.
341
+ def _define_runner(symbol, str, options)
342
+ self.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
343
+ def _run_#{symbol}_callbacks(key = nil)
344
+ if key
345
+ send("_run_\#{self.class}_#{symbol}_\#{key}_callbacks") { yield }
346
+ else
347
+ #{str}
348
+ end
349
+ end
350
+ RUBY_EVAL
351
+
352
+ before_name, around_name, after_name =
353
+ options.values_at(:before, :after, :around)
354
+ end
355
+
356
+ # This is called the first time a callback is called with a particular
357
+ # key. It creates a new callback method for the key, calculating
358
+ # which callbacks can be omitted because of per_key conditions.
359
+ def _create_and_run_keyed_callback(klass, kind, key, obj, &blk)
360
+ @_keyed_callbacks ||= {}
361
+ @_keyed_callbacks[[kind, key]] ||= begin
362
+ str = self.send("_#{kind}_callbacks").compile(key, obj)
363
+ self.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
364
+ def _run_#{klass}_#{kind}_#{key}_callbacks
365
+ #{str}
366
+ end
367
+ RUBY_EVAL
368
+ true
369
+ end
370
+ obj.send("_run_#{klass}_#{kind}_#{key}_callbacks", &blk)
371
+ end
372
+
373
+ # Define callbacks.
374
+ #
375
+ # Creates a <name>_callback method that you can use to add callbacks.
376
+ #
377
+ # Syntax:
378
+ # save_callback :before, :before_meth
379
+ # save_callback :after, :after_meth, :if => :condition
380
+ # save_callback :around {|r| stuff; yield; stuff }
381
+ #
382
+ # The <name>_callback method also updates the _run_<name>_callbacks
383
+ # method, which is the public API to run the callbacks.
384
+ #
385
+ # Also creates a skip_<name>_callback method that you can use to skip
386
+ # callbacks.
387
+ #
388
+ # When creating or skipping callbacks, you can specify conditions that
389
+ # are always the same for a given key. For instance, in ActionPack,
390
+ # we convert :only and :except conditions into per-key conditions.
391
+ #
392
+ # before_filter :authenticate, :except => "index"
393
+ # becomes
394
+ # dispatch_callback :before, :authenticate, :per_key => {:unless => proc {|c| c.action_name == "index"}}
395
+ #
396
+ # Per-Key conditions are evaluated only once per use of a given key.
397
+ # In the case of the above example, you would do:
398
+ #
399
+ # run_dispatch_callbacks(action_name) { ... dispatch stuff ... }
400
+ #
401
+ # In that case, each action_name would get its own compiled callback
402
+ # method that took into consideration the per_key conditions. This
403
+ # is a speed improvement for ActionPack.
404
+ def define_callbacks(*symbols)
405
+ symbols.each do |symbol|
406
+ self.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
407
+ class_inheritable_accessor :_#{symbol}_callbacks
408
+ self._#{symbol}_callbacks = CallbackChain.new
409
+
410
+ def self.#{symbol}_callback(type, *filters, &blk)
411
+ options = filters.last.is_a?(Hash) ? filters.pop : {}
412
+ filters.unshift(blk) if block_given?
413
+ filters.map! {|f| Callback.new(f, type, options.dup, self, :#{symbol})}
414
+ self._#{symbol}_callbacks.push(*filters)
415
+ _define_runner(:#{symbol}, self._#{symbol}_callbacks.compile, options)
416
+ end
417
+
418
+ def self.skip_#{symbol}_callback(type, *filters, &blk)
419
+ options = filters.last.is_a?(Hash) ? filters.pop : {}
420
+ filters.unshift(blk) if block_given?
421
+ filters.each do |filter|
422
+ self._#{symbol}_callbacks = self._#{symbol}_callbacks.clone(self)
423
+
424
+ filter = self._#{symbol}_callbacks.find {|c| c.matches?(type, :#{symbol}, filter) }
425
+ per_key = options[:per_key] || {}
426
+ if filter
427
+ filter.recompile!(options, per_key)
428
+ else
429
+ self._#{symbol}_callbacks.delete(filter)
430
+ end
431
+ _define_runner(:#{symbol}, self._#{symbol}_callbacks.compile, options)
432
+ end
433
+
434
+ end
435
+
436
+ self.#{symbol}_callback(:before)
437
+ RUBY_EVAL
438
+ end
439
+ end
440
+ end
441
+ end
442
+ end