brianmario-couchrest 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 (92) hide show
  1. data/LICENSE +176 -0
  2. data/README.md +95 -0
  3. data/Rakefile +75 -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 +198 -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 +303 -0
  20. data/lib/couchrest/core/design.rb +79 -0
  21. data/lib/couchrest/core/document.rb +87 -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/helper/upgrade.rb +51 -0
  28. data/lib/couchrest/mixins.rb +4 -0
  29. data/lib/couchrest/mixins/attachments.rb +31 -0
  30. data/lib/couchrest/mixins/callbacks.rb +483 -0
  31. data/lib/couchrest/mixins/class_proxy.rb +108 -0
  32. data/lib/couchrest/mixins/design_doc.rb +90 -0
  33. data/lib/couchrest/mixins/document_queries.rb +44 -0
  34. data/lib/couchrest/mixins/extended_attachments.rb +68 -0
  35. data/lib/couchrest/mixins/extended_document_mixins.rb +7 -0
  36. data/lib/couchrest/mixins/properties.rb +129 -0
  37. data/lib/couchrest/mixins/validation.rb +242 -0
  38. data/lib/couchrest/mixins/views.rb +169 -0
  39. data/lib/couchrest/monkeypatches.rb +113 -0
  40. data/lib/couchrest/more/casted_model.rb +28 -0
  41. data/lib/couchrest/more/extended_document.rb +215 -0
  42. data/lib/couchrest/more/property.rb +40 -0
  43. data/lib/couchrest/support/blank.rb +42 -0
  44. data/lib/couchrest/support/class.rb +176 -0
  45. data/lib/couchrest/validation/auto_validate.rb +163 -0
  46. data/lib/couchrest/validation/contextual_validators.rb +78 -0
  47. data/lib/couchrest/validation/validation_errors.rb +118 -0
  48. data/lib/couchrest/validation/validators/absent_field_validator.rb +74 -0
  49. data/lib/couchrest/validation/validators/confirmation_validator.rb +99 -0
  50. data/lib/couchrest/validation/validators/format_validator.rb +117 -0
  51. data/lib/couchrest/validation/validators/formats/email.rb +66 -0
  52. data/lib/couchrest/validation/validators/formats/url.rb +43 -0
  53. data/lib/couchrest/validation/validators/generic_validator.rb +120 -0
  54. data/lib/couchrest/validation/validators/length_validator.rb +134 -0
  55. data/lib/couchrest/validation/validators/method_validator.rb +89 -0
  56. data/lib/couchrest/validation/validators/numeric_validator.rb +104 -0
  57. data/lib/couchrest/validation/validators/required_field_validator.rb +109 -0
  58. data/spec/couchrest/core/couchrest_spec.rb +201 -0
  59. data/spec/couchrest/core/database_spec.rb +699 -0
  60. data/spec/couchrest/core/design_spec.rb +138 -0
  61. data/spec/couchrest/core/document_spec.rb +267 -0
  62. data/spec/couchrest/core/server_spec.rb +35 -0
  63. data/spec/couchrest/helpers/pager_spec.rb +122 -0
  64. data/spec/couchrest/helpers/streamer_spec.rb +23 -0
  65. data/spec/couchrest/more/casted_extended_doc_spec.rb +40 -0
  66. data/spec/couchrest/more/casted_model_spec.rb +98 -0
  67. data/spec/couchrest/more/extended_doc_attachment_spec.rb +130 -0
  68. data/spec/couchrest/more/extended_doc_spec.rb +509 -0
  69. data/spec/couchrest/more/extended_doc_subclass_spec.rb +98 -0
  70. data/spec/couchrest/more/extended_doc_view_spec.rb +355 -0
  71. data/spec/couchrest/more/property_spec.rb +136 -0
  72. data/spec/fixtures/attachments/README +3 -0
  73. data/spec/fixtures/attachments/couchdb.png +0 -0
  74. data/spec/fixtures/attachments/test.html +11 -0
  75. data/spec/fixtures/more/article.rb +34 -0
  76. data/spec/fixtures/more/card.rb +20 -0
  77. data/spec/fixtures/more/course.rb +14 -0
  78. data/spec/fixtures/more/event.rb +6 -0
  79. data/spec/fixtures/more/invoice.rb +17 -0
  80. data/spec/fixtures/more/person.rb +8 -0
  81. data/spec/fixtures/more/question.rb +6 -0
  82. data/spec/fixtures/more/service.rb +12 -0
  83. data/spec/fixtures/views/lib.js +3 -0
  84. data/spec/fixtures/views/test_view/lib.js +3 -0
  85. data/spec/fixtures/views/test_view/only-map.js +4 -0
  86. data/spec/fixtures/views/test_view/test-map.js +3 -0
  87. data/spec/fixtures/views/test_view/test-reduce.js +3 -0
  88. data/spec/spec.opts +6 -0
  89. data/spec/spec_helper.rb +26 -0
  90. data/utils/remap.rb +27 -0
  91. data/utils/subset.rb +30 -0
  92. metadata +200 -0
@@ -0,0 +1,87 @@
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(:extlib_inheritable_accessor, :database)
9
+ # end
10
+
11
+ extlib_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
+ # 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
78
+ end
79
+
80
+ # Returns the document's database
81
+ def database
82
+ @database || self.class.database
83
+ end
84
+
85
+ end
86
+
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
@@ -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
+ Yajl::Parser.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
+ Yajl::Parser.parse("#{line}}")
39
+ rescue
40
+ nil
41
+ end
42
+
43
+ end
44
+ 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