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.
- data/LICENSE +176 -0
- data/README.md +95 -0
- data/Rakefile +75 -0
- data/THANKS.md +18 -0
- data/examples/model/example.rb +144 -0
- data/examples/word_count/markov +38 -0
- data/examples/word_count/views/books/chunked-map.js +3 -0
- data/examples/word_count/views/books/united-map.js +1 -0
- data/examples/word_count/views/markov/chain-map.js +6 -0
- data/examples/word_count/views/markov/chain-reduce.js +7 -0
- data/examples/word_count/views/word_count/count-map.js +6 -0
- data/examples/word_count/views/word_count/count-reduce.js +3 -0
- data/examples/word_count/word_count.rb +46 -0
- data/examples/word_count/word_count_query.rb +40 -0
- data/examples/word_count/word_count_views.rb +26 -0
- data/lib/couchrest.rb +198 -0
- data/lib/couchrest/commands/generate.rb +71 -0
- data/lib/couchrest/commands/push.rb +103 -0
- data/lib/couchrest/core/database.rb +303 -0
- data/lib/couchrest/core/design.rb +79 -0
- data/lib/couchrest/core/document.rb +87 -0
- data/lib/couchrest/core/response.rb +16 -0
- data/lib/couchrest/core/server.rb +88 -0
- data/lib/couchrest/core/view.rb +4 -0
- data/lib/couchrest/helper/pager.rb +103 -0
- data/lib/couchrest/helper/streamer.rb +44 -0
- data/lib/couchrest/helper/upgrade.rb +51 -0
- data/lib/couchrest/mixins.rb +4 -0
- data/lib/couchrest/mixins/attachments.rb +31 -0
- data/lib/couchrest/mixins/callbacks.rb +483 -0
- data/lib/couchrest/mixins/class_proxy.rb +108 -0
- data/lib/couchrest/mixins/design_doc.rb +90 -0
- data/lib/couchrest/mixins/document_queries.rb +44 -0
- data/lib/couchrest/mixins/extended_attachments.rb +68 -0
- data/lib/couchrest/mixins/extended_document_mixins.rb +7 -0
- data/lib/couchrest/mixins/properties.rb +129 -0
- data/lib/couchrest/mixins/validation.rb +242 -0
- data/lib/couchrest/mixins/views.rb +169 -0
- data/lib/couchrest/monkeypatches.rb +113 -0
- data/lib/couchrest/more/casted_model.rb +28 -0
- data/lib/couchrest/more/extended_document.rb +215 -0
- data/lib/couchrest/more/property.rb +40 -0
- data/lib/couchrest/support/blank.rb +42 -0
- data/lib/couchrest/support/class.rb +176 -0
- data/lib/couchrest/validation/auto_validate.rb +163 -0
- data/lib/couchrest/validation/contextual_validators.rb +78 -0
- data/lib/couchrest/validation/validation_errors.rb +118 -0
- data/lib/couchrest/validation/validators/absent_field_validator.rb +74 -0
- data/lib/couchrest/validation/validators/confirmation_validator.rb +99 -0
- data/lib/couchrest/validation/validators/format_validator.rb +117 -0
- data/lib/couchrest/validation/validators/formats/email.rb +66 -0
- data/lib/couchrest/validation/validators/formats/url.rb +43 -0
- data/lib/couchrest/validation/validators/generic_validator.rb +120 -0
- data/lib/couchrest/validation/validators/length_validator.rb +134 -0
- data/lib/couchrest/validation/validators/method_validator.rb +89 -0
- data/lib/couchrest/validation/validators/numeric_validator.rb +104 -0
- data/lib/couchrest/validation/validators/required_field_validator.rb +109 -0
- data/spec/couchrest/core/couchrest_spec.rb +201 -0
- data/spec/couchrest/core/database_spec.rb +699 -0
- data/spec/couchrest/core/design_spec.rb +138 -0
- data/spec/couchrest/core/document_spec.rb +267 -0
- data/spec/couchrest/core/server_spec.rb +35 -0
- data/spec/couchrest/helpers/pager_spec.rb +122 -0
- data/spec/couchrest/helpers/streamer_spec.rb +23 -0
- data/spec/couchrest/more/casted_extended_doc_spec.rb +40 -0
- data/spec/couchrest/more/casted_model_spec.rb +98 -0
- data/spec/couchrest/more/extended_doc_attachment_spec.rb +130 -0
- data/spec/couchrest/more/extended_doc_spec.rb +509 -0
- data/spec/couchrest/more/extended_doc_subclass_spec.rb +98 -0
- data/spec/couchrest/more/extended_doc_view_spec.rb +355 -0
- data/spec/couchrest/more/property_spec.rb +136 -0
- data/spec/fixtures/attachments/README +3 -0
- data/spec/fixtures/attachments/couchdb.png +0 -0
- data/spec/fixtures/attachments/test.html +11 -0
- data/spec/fixtures/more/article.rb +34 -0
- data/spec/fixtures/more/card.rb +20 -0
- data/spec/fixtures/more/course.rb +14 -0
- data/spec/fixtures/more/event.rb +6 -0
- data/spec/fixtures/more/invoice.rb +17 -0
- data/spec/fixtures/more/person.rb +8 -0
- data/spec/fixtures/more/question.rb +6 -0
- data/spec/fixtures/more/service.rb +12 -0
- data/spec/fixtures/views/lib.js +3 -0
- data/spec/fixtures/views/test_view/lib.js +3 -0
- data/spec/fixtures/views/test_view/only-map.js +4 -0
- data/spec/fixtures/views/test_view/test-map.js +3 -0
- data/spec/fixtures/views/test_view/test-reduce.js +3 -0
- data/spec/spec.opts +6 -0
- data/spec/spec_helper.rb +26 -0
- data/utils/remap.rb +27 -0
- data/utils/subset.rb +30 -0
- 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,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,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,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
|