gohanlonllc-couchrest 0.2.3.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (92) hide show
  1. data/LICENSE +176 -0
  2. data/README.md +93 -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 +298 -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 +694 -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
+ 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,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