couchrest 0.38 → 1.0.0.beta
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/README.md +8 -8
- data/Rakefile +3 -4
- data/couchrest.gemspec +25 -105
- data/history.txt +5 -4
- data/lib/couchrest.rb +31 -52
- data/lib/couchrest/{core/database.rb → database.rb} +6 -11
- data/lib/couchrest/{core/design.rb → design.rb} +2 -2
- data/lib/couchrest/{core/document.rb → document.rb} +1 -1
- data/lib/couchrest/helper/attachments.rb +29 -0
- data/lib/couchrest/middlewares/logger.rb +3 -3
- data/lib/couchrest/monkeypatches.rb +1 -71
- data/lib/couchrest/{core/response.rb → response.rb} +0 -0
- data/lib/couchrest/{core/rest_api.rb → rest_api.rb} +8 -12
- data/lib/couchrest/{core/server.rb → server.rb} +0 -2
- data/spec/couchrest/{core/couchrest_spec.rb → couchrest_spec.rb} +15 -9
- data/spec/couchrest/{core/database_spec.rb → database_spec.rb} +4 -4
- data/spec/couchrest/{core/design_spec.rb → design_spec.rb} +2 -2
- data/spec/couchrest/{core/document_spec.rb → document_spec.rb} +1 -1
- data/spec/couchrest/{core/server_spec.rb → server_spec.rb} +2 -2
- data/spec/spec.opts +0 -1
- data/spec/spec_helper.rb +0 -4
- metadata +32 -133
- data/examples/model/example.rb +0 -144
- data/lib/couchrest/core/adapters/restclient.rb +0 -35
- data/lib/couchrest/core/http_abstraction.rb +0 -48
- data/lib/couchrest/core/view.rb +0 -4
- data/lib/couchrest/mixins.rb +0 -4
- data/lib/couchrest/mixins/attachments.rb +0 -31
- data/lib/couchrest/mixins/attribute_protection.rb +0 -74
- data/lib/couchrest/mixins/callbacks.rb +0 -532
- data/lib/couchrest/mixins/class_proxy.rb +0 -124
- data/lib/couchrest/mixins/collection.rb +0 -260
- data/lib/couchrest/mixins/design_doc.rb +0 -103
- data/lib/couchrest/mixins/document_queries.rb +0 -80
- data/lib/couchrest/mixins/extended_attachments.rb +0 -70
- data/lib/couchrest/mixins/extended_document_mixins.rb +0 -9
- data/lib/couchrest/mixins/properties.rb +0 -158
- data/lib/couchrest/mixins/validation.rb +0 -246
- data/lib/couchrest/mixins/views.rb +0 -173
- data/lib/couchrest/more/casted_model.rb +0 -58
- data/lib/couchrest/more/extended_document.rb +0 -310
- data/lib/couchrest/more/property.rb +0 -58
- data/lib/couchrest/more/typecast.rb +0 -180
- data/lib/couchrest/support/blank.rb +0 -42
- data/lib/couchrest/support/rails.rb +0 -42
- data/lib/couchrest/validation/auto_validate.rb +0 -157
- data/lib/couchrest/validation/contextual_validators.rb +0 -78
- data/lib/couchrest/validation/validation_errors.rb +0 -125
- data/lib/couchrest/validation/validators/absent_field_validator.rb +0 -74
- data/lib/couchrest/validation/validators/confirmation_validator.rb +0 -107
- data/lib/couchrest/validation/validators/format_validator.rb +0 -122
- data/lib/couchrest/validation/validators/formats/email.rb +0 -66
- data/lib/couchrest/validation/validators/formats/url.rb +0 -43
- data/lib/couchrest/validation/validators/generic_validator.rb +0 -120
- data/lib/couchrest/validation/validators/length_validator.rb +0 -139
- data/lib/couchrest/validation/validators/method_validator.rb +0 -89
- data/lib/couchrest/validation/validators/numeric_validator.rb +0 -109
- data/lib/couchrest/validation/validators/required_field_validator.rb +0 -114
- data/spec/couchrest/more/attribute_protection_spec.rb +0 -150
- data/spec/couchrest/more/casted_extended_doc_spec.rb +0 -73
- data/spec/couchrest/more/casted_model_spec.rb +0 -406
- data/spec/couchrest/more/extended_doc_attachment_spec.rb +0 -135
- data/spec/couchrest/more/extended_doc_inherited_spec.rb +0 -40
- data/spec/couchrest/more/extended_doc_spec.rb +0 -807
- data/spec/couchrest/more/extended_doc_subclass_spec.rb +0 -98
- data/spec/couchrest/more/extended_doc_view_spec.rb +0 -456
- data/spec/couchrest/more/property_spec.rb +0 -628
- data/spec/fixtures/more/article.rb +0 -35
- data/spec/fixtures/more/card.rb +0 -22
- data/spec/fixtures/more/cat.rb +0 -20
- data/spec/fixtures/more/course.rb +0 -22
- data/spec/fixtures/more/event.rb +0 -8
- data/spec/fixtures/more/invoice.rb +0 -17
- data/spec/fixtures/more/person.rb +0 -9
- data/spec/fixtures/more/question.rb +0 -6
- data/spec/fixtures/more/service.rb +0 -12
- data/spec/fixtures/more/user.rb +0 -22
data/examples/model/example.rb
DELETED
@@ -1,144 +0,0 @@
|
|
1
|
-
require File.join(File.dirname(__FILE__), '..', '..', 'lib', 'couchrest')
|
2
|
-
|
3
|
-
def show obj
|
4
|
-
puts obj.inspect
|
5
|
-
puts
|
6
|
-
end
|
7
|
-
|
8
|
-
SERVER = CouchRest.new
|
9
|
-
SERVER.default_database = 'couchrest-extendeddoc-example'
|
10
|
-
|
11
|
-
class Author < CouchRest::ExtendedDocument
|
12
|
-
use_database SERVER.default_database
|
13
|
-
property :name
|
14
|
-
|
15
|
-
def drink_scotch
|
16
|
-
puts "... glug type glug ... I'm #{name} ... type glug glug ..."
|
17
|
-
end
|
18
|
-
end
|
19
|
-
|
20
|
-
class Post < CouchRest::ExtendedDocument
|
21
|
-
use_database SERVER.default_database
|
22
|
-
|
23
|
-
property :title
|
24
|
-
property :body
|
25
|
-
property :author, :cast_as => 'Author'
|
26
|
-
|
27
|
-
timestamps!
|
28
|
-
end
|
29
|
-
|
30
|
-
class Comment < CouchRest::ExtendedDocument
|
31
|
-
use_database SERVER.default_database
|
32
|
-
|
33
|
-
property :commenter, :cast_as => 'Author'
|
34
|
-
timestamps!
|
35
|
-
|
36
|
-
def post= post
|
37
|
-
self["post_id"] = post.id
|
38
|
-
end
|
39
|
-
def post
|
40
|
-
Post.get(self['post_id']) if self['post_id']
|
41
|
-
end
|
42
|
-
|
43
|
-
end
|
44
|
-
|
45
|
-
puts "Act I: CRUD"
|
46
|
-
puts
|
47
|
-
puts "(pause for dramatic effect)"
|
48
|
-
puts
|
49
|
-
sleep 2
|
50
|
-
|
51
|
-
puts "Create an author."
|
52
|
-
quentin = Author.new("name" => "Quentin Hazel")
|
53
|
-
show quentin
|
54
|
-
|
55
|
-
puts "Create a new post."
|
56
|
-
post = Post.new(:title => "First Post", :body => "Lorem ipsum dolor sit amet, consectetur adipisicing elit...")
|
57
|
-
show post
|
58
|
-
|
59
|
-
puts "Add the author to the post."
|
60
|
-
post.author = quentin
|
61
|
-
show post
|
62
|
-
|
63
|
-
puts "Save the post."
|
64
|
-
post.save
|
65
|
-
show post
|
66
|
-
|
67
|
-
puts "Load the post."
|
68
|
-
reloaded = Post.get(post.id)
|
69
|
-
show reloaded
|
70
|
-
|
71
|
-
puts "The author of the post is an instance of Author."
|
72
|
-
reloaded.author.drink_scotch
|
73
|
-
|
74
|
-
puts "\nAdd some comments to the post."
|
75
|
-
comment_one = Comment.new :text => "Blah blah blah", :commenter => {:name => "Joe Sixpack"}
|
76
|
-
comment_two = Comment.new :text => "Yeah yeah yeah", :commenter => {:name => "Jane Doe"}
|
77
|
-
comment_three = Comment.new :text => "Whatever...", :commenter => {:name => "John Stewart"}
|
78
|
-
|
79
|
-
# TODO - maybe add some magic here?
|
80
|
-
comment_one.post = post
|
81
|
-
comment_two.post = post
|
82
|
-
comment_three.post = post
|
83
|
-
comment_one.save
|
84
|
-
comment_two.save
|
85
|
-
comment_three.save
|
86
|
-
|
87
|
-
show comment_one
|
88
|
-
show comment_two
|
89
|
-
show comment_three
|
90
|
-
|
91
|
-
puts "We can load a post through its comment (no magic here)."
|
92
|
-
show post = comment_one.post
|
93
|
-
|
94
|
-
puts "Commenters are also authors."
|
95
|
-
comment_two['commenter'].drink_scotch
|
96
|
-
comment_one['commenter'].drink_scotch
|
97
|
-
comment_three['commenter'].drink_scotch
|
98
|
-
|
99
|
-
puts "\nLet's save an author to her own document."
|
100
|
-
jane = comment_two['commenter']
|
101
|
-
jane.save
|
102
|
-
show jane
|
103
|
-
|
104
|
-
puts "Oh, that's neat! Because Ruby passes hash valuee by reference, Jane's new id has been added to the comment she left."
|
105
|
-
show comment_two
|
106
|
-
|
107
|
-
puts "Of course, we'd better remember to save it."
|
108
|
-
comment_two.save
|
109
|
-
show comment_two
|
110
|
-
|
111
|
-
puts "Oooh, denormalized... feel the burn!"
|
112
|
-
puts
|
113
|
-
puts
|
114
|
-
puts
|
115
|
-
puts "Act II: Views"
|
116
|
-
puts
|
117
|
-
puts
|
118
|
-
sleep 2
|
119
|
-
|
120
|
-
puts "Let's find all the comments that go with our post."
|
121
|
-
puts "Our post has id #{post.id}, so lets find all the comments with that post_id."
|
122
|
-
puts
|
123
|
-
|
124
|
-
class Comment
|
125
|
-
view_by :post_id
|
126
|
-
end
|
127
|
-
|
128
|
-
comments = Comment.by_post_id :key => post.id
|
129
|
-
show comments
|
130
|
-
|
131
|
-
puts "That was too easy."
|
132
|
-
puts "We can even wrap it up in a finder on the Post class."
|
133
|
-
puts
|
134
|
-
|
135
|
-
class Post
|
136
|
-
def comments
|
137
|
-
Comment.by_post_id :key => id
|
138
|
-
end
|
139
|
-
end
|
140
|
-
|
141
|
-
show post.comments
|
142
|
-
puts "Gimme 5 minutes and I'll roll this into the framework. ;)"
|
143
|
-
puts
|
144
|
-
puts "There is a lot more that can be done with views, but a lot of the interesting stuff is joins, which of course range across types. We'll pick up where we left off, next time."
|
@@ -1,35 +0,0 @@
|
|
1
|
-
module RestClientAdapter
|
2
|
-
|
3
|
-
module API
|
4
|
-
def proxy=(url)
|
5
|
-
RestClient.proxy = url
|
6
|
-
end
|
7
|
-
|
8
|
-
def proxy
|
9
|
-
RestClient.proxy
|
10
|
-
end
|
11
|
-
|
12
|
-
def get(uri, headers={})
|
13
|
-
RestClient.get(uri, headers).to_s
|
14
|
-
end
|
15
|
-
|
16
|
-
def post(uri, payload, headers={})
|
17
|
-
RestClient.post(uri, payload, headers).to_s
|
18
|
-
end
|
19
|
-
|
20
|
-
def put(uri, payload, headers={})
|
21
|
-
RestClient.put(uri, payload, headers).to_s
|
22
|
-
end
|
23
|
-
|
24
|
-
def delete(uri, headers={})
|
25
|
-
RestClient.delete(uri, headers).to_s
|
26
|
-
end
|
27
|
-
|
28
|
-
def copy(uri, headers)
|
29
|
-
RestClient::Request.execute( :method => :copy,
|
30
|
-
:url => uri,
|
31
|
-
:headers => headers).to_s
|
32
|
-
end
|
33
|
-
end
|
34
|
-
|
35
|
-
end
|
@@ -1,48 +0,0 @@
|
|
1
|
-
require 'couchrest/core/adapters/restclient'
|
2
|
-
|
3
|
-
# Abstraction layet for HTTP communications.
|
4
|
-
#
|
5
|
-
# By defining a basic API that CouchRest is relying on,
|
6
|
-
# it allows for easy experimentations and implementations of various libraries.
|
7
|
-
#
|
8
|
-
# Most of the API is based on the RestClient API that was used in the early version of CouchRest.
|
9
|
-
#
|
10
|
-
module HttpAbstraction
|
11
|
-
|
12
|
-
# here is the list of exception expected by CouchRest
|
13
|
-
# please convert the underlying errors in this set of known
|
14
|
-
# exceptions.
|
15
|
-
class ResourceNotFound < StandardError; end
|
16
|
-
class RequestFailed < StandardError; end
|
17
|
-
class RequestTimeout < StandardError; end
|
18
|
-
class ServerBrokeConnection < StandardError; end
|
19
|
-
class Conflict < StandardError; end
|
20
|
-
|
21
|
-
|
22
|
-
# # Here is the API you need to implement if you want to write a new adapter
|
23
|
-
# # See adapters/restclient.rb for more information.
|
24
|
-
#
|
25
|
-
# def self.proxy=(url)
|
26
|
-
# end
|
27
|
-
#
|
28
|
-
# def self.proxy
|
29
|
-
# end
|
30
|
-
#
|
31
|
-
# def self.get(uri, headers=nil)
|
32
|
-
# end
|
33
|
-
#
|
34
|
-
# def self.post(uri, payload, headers=nil)
|
35
|
-
# end
|
36
|
-
#
|
37
|
-
# def self.put(uri, payload, headers=nil)
|
38
|
-
# end
|
39
|
-
#
|
40
|
-
# def self.delete(uri, headers=nil)
|
41
|
-
# end
|
42
|
-
#
|
43
|
-
# def self.copy(uri, headers)
|
44
|
-
# end
|
45
|
-
|
46
|
-
end
|
47
|
-
|
48
|
-
HttpAbstraction.extend(RestClientAdapter::API)
|
data/lib/couchrest/core/view.rb
DELETED
data/lib/couchrest/mixins.rb
DELETED
@@ -1,31 +0,0 @@
|
|
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, force=false)
|
23
|
-
raise ArgumentError, "doc.database required to delete_attachment" unless database
|
24
|
-
result = database.delete_attachment(self, name, force)
|
25
|
-
self['_rev'] = result['rev']
|
26
|
-
result['ok']
|
27
|
-
end
|
28
|
-
|
29
|
-
end
|
30
|
-
end
|
31
|
-
end
|
@@ -1,74 +0,0 @@
|
|
1
|
-
module CouchRest
|
2
|
-
module Mixins
|
3
|
-
module AttributeProtection
|
4
|
-
# Attribute protection from mass assignment to CouchRest properties
|
5
|
-
#
|
6
|
-
# Protected methods will be removed from
|
7
|
-
# * new
|
8
|
-
# * update_attributes
|
9
|
-
# * upate_attributes_without_saving
|
10
|
-
# * attributes=
|
11
|
-
#
|
12
|
-
# There are two modes of protection
|
13
|
-
# 1) Declare accessible poperties, assume all the rest are protected
|
14
|
-
# property :name, :accessible => true
|
15
|
-
# property :admin # this will be automatically protected
|
16
|
-
#
|
17
|
-
# 2) Declare protected properties, assume all the rest are accessible
|
18
|
-
# property :name # this will not be protected
|
19
|
-
# property :admin, :protected => true
|
20
|
-
#
|
21
|
-
# Note: you cannot set both flags in a single class
|
22
|
-
|
23
|
-
def self.included(base)
|
24
|
-
base.extend(ClassMethods)
|
25
|
-
end
|
26
|
-
|
27
|
-
module ClassMethods
|
28
|
-
def accessible_properties
|
29
|
-
properties.select { |prop| prop.options[:accessible] }
|
30
|
-
end
|
31
|
-
|
32
|
-
def protected_properties
|
33
|
-
properties.select { |prop| prop.options[:protected] }
|
34
|
-
end
|
35
|
-
end
|
36
|
-
|
37
|
-
def accessible_properties
|
38
|
-
self.class.accessible_properties
|
39
|
-
end
|
40
|
-
|
41
|
-
def protected_properties
|
42
|
-
self.class.protected_properties
|
43
|
-
end
|
44
|
-
|
45
|
-
def remove_protected_attributes(attributes)
|
46
|
-
protected_names = properties_to_remove_from_mass_assignment.map { |prop| prop.name }
|
47
|
-
return attributes if protected_names.empty?
|
48
|
-
|
49
|
-
attributes.reject! do |property_name, property_value|
|
50
|
-
protected_names.include?(property_name.to_s)
|
51
|
-
end
|
52
|
-
|
53
|
-
attributes || {}
|
54
|
-
end
|
55
|
-
|
56
|
-
private
|
57
|
-
|
58
|
-
def properties_to_remove_from_mass_assignment
|
59
|
-
has_protected = !protected_properties.empty?
|
60
|
-
has_accessible = !accessible_properties.empty?
|
61
|
-
|
62
|
-
if !has_protected && !has_accessible
|
63
|
-
[]
|
64
|
-
elsif has_protected && !has_accessible
|
65
|
-
protected_properties
|
66
|
-
elsif has_accessible && !has_protected
|
67
|
-
properties.reject { |prop| prop.options[:accessible] }
|
68
|
-
else
|
69
|
-
raise "Set either :accessible or :protected for #{self.class}, but not both"
|
70
|
-
end
|
71
|
-
end
|
72
|
-
end
|
73
|
-
end
|
74
|
-
end
|
@@ -1,532 +0,0 @@
|
|
1
|
-
# Copyright (c) 2006-2009 David Heinemeier Hansson
|
2
|
-
#
|
3
|
-
# Permission is hereby granted, free of charge, to any person obtaining
|
4
|
-
# a copy of this software and associated documentation files (the
|
5
|
-
# "Software"), to deal in the Software without restriction, including
|
6
|
-
# without limitation the rights to use, copy, modify, merge, publish,
|
7
|
-
# distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
-
# permit persons to whom the Software is furnished to do so, subject to
|
9
|
-
# the following conditions:
|
10
|
-
#
|
11
|
-
# The above copyright notice and this permission notice shall be
|
12
|
-
# included in all copies or substantial portions of the Software.
|
13
|
-
#
|
14
|
-
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
-
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
-
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
-
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
-
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
-
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
-
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
21
|
-
#
|
22
|
-
# Extracted from ActiveSupport::NewCallbacks written by Yehuda Katz
|
23
|
-
# http://github.com/rails/rails/raw/d6e4113c83a9d55be6f2af247da2cecaa855f43b/activesupport/lib/active_support/new_callbacks.rb
|
24
|
-
# http://github.com/rails/rails/commit/1126a85aed576402d978e6f76eb393b6baaa9541
|
25
|
-
|
26
|
-
require File.join(File.dirname(__FILE__), '..', 'support', 'class')
|
27
|
-
|
28
|
-
module CouchRest
|
29
|
-
# Callbacks are hooks into the lifecycle of an object that allow you to trigger logic
|
30
|
-
# before or after an alteration of the object state.
|
31
|
-
#
|
32
|
-
# Mixing in this module allows you to define callbacks in your class.
|
33
|
-
#
|
34
|
-
# Example:
|
35
|
-
# class Storage
|
36
|
-
# include ActiveSupport::Callbacks
|
37
|
-
#
|
38
|
-
# define_callbacks :save
|
39
|
-
# end
|
40
|
-
#
|
41
|
-
# class ConfigStorage < Storage
|
42
|
-
# save_callback :before, :saving_message
|
43
|
-
# def saving_message
|
44
|
-
# puts "saving..."
|
45
|
-
# end
|
46
|
-
#
|
47
|
-
# save_callback :after do |object|
|
48
|
-
# puts "saved"
|
49
|
-
# end
|
50
|
-
#
|
51
|
-
# def save
|
52
|
-
# _run_save_callbacks do
|
53
|
-
# puts "- save"
|
54
|
-
# end
|
55
|
-
# end
|
56
|
-
# end
|
57
|
-
#
|
58
|
-
# config = ConfigStorage.new
|
59
|
-
# config.save
|
60
|
-
#
|
61
|
-
# Output:
|
62
|
-
# saving...
|
63
|
-
# - save
|
64
|
-
# saved
|
65
|
-
#
|
66
|
-
# Callbacks from parent classes are inherited.
|
67
|
-
#
|
68
|
-
# Example:
|
69
|
-
# class Storage
|
70
|
-
# include ActiveSupport::Callbacks
|
71
|
-
#
|
72
|
-
# define_callbacks :save
|
73
|
-
#
|
74
|
-
# save_callback :before, :prepare
|
75
|
-
# def prepare
|
76
|
-
# puts "preparing save"
|
77
|
-
# end
|
78
|
-
# end
|
79
|
-
#
|
80
|
-
# class ConfigStorage < Storage
|
81
|
-
# save_callback :before, :saving_message
|
82
|
-
# def saving_message
|
83
|
-
# puts "saving..."
|
84
|
-
# end
|
85
|
-
#
|
86
|
-
# save_callback :after do |object|
|
87
|
-
# puts "saved"
|
88
|
-
# end
|
89
|
-
#
|
90
|
-
# def save
|
91
|
-
# _run_save_callbacks do
|
92
|
-
# puts "- save"
|
93
|
-
# end
|
94
|
-
# end
|
95
|
-
# end
|
96
|
-
#
|
97
|
-
# config = ConfigStorage.new
|
98
|
-
# config.save
|
99
|
-
#
|
100
|
-
# Output:
|
101
|
-
# preparing save
|
102
|
-
# saving...
|
103
|
-
# - save
|
104
|
-
# saved
|
105
|
-
module Callbacks
|
106
|
-
def self.included(klass)
|
107
|
-
klass.extend ClassMethods
|
108
|
-
end
|
109
|
-
|
110
|
-
def run_callbacks(kind, options = {}, &blk)
|
111
|
-
send("_run_#{kind}_callbacks", &blk)
|
112
|
-
end
|
113
|
-
|
114
|
-
class Callback
|
115
|
-
@@_callback_sequence = 0
|
116
|
-
|
117
|
-
attr_accessor :filter, :kind, :name, :options, :per_key, :klass
|
118
|
-
def initialize(filter, kind, options, klass)
|
119
|
-
@kind, @klass = kind, klass
|
120
|
-
|
121
|
-
normalize_options!(options)
|
122
|
-
|
123
|
-
@per_key = options.delete(:per_key)
|
124
|
-
@raw_filter, @options = filter, options
|
125
|
-
@filter = _compile_filter(filter)
|
126
|
-
@compiled_options = _compile_options(options)
|
127
|
-
@callback_id = next_id
|
128
|
-
|
129
|
-
_compile_per_key_options
|
130
|
-
end
|
131
|
-
|
132
|
-
def clone(klass)
|
133
|
-
obj = super()
|
134
|
-
obj.klass = klass
|
135
|
-
obj.per_key = @per_key.dup
|
136
|
-
obj.options = @options.dup
|
137
|
-
obj.per_key[:if] = @per_key[:if].dup
|
138
|
-
obj.per_key[:unless] = @per_key[:unless].dup
|
139
|
-
obj.options[:if] = @options[:if].dup
|
140
|
-
obj.options[:unless] = @options[:unless].dup
|
141
|
-
obj
|
142
|
-
end
|
143
|
-
|
144
|
-
def normalize_options!(options)
|
145
|
-
options[:if] = Array.wrap(options[:if])
|
146
|
-
options[:unless] = Array.wrap(options[:unless])
|
147
|
-
|
148
|
-
options[:per_key] ||= {}
|
149
|
-
options[:per_key][:if] = Array.wrap(options[:per_key][:if])
|
150
|
-
options[:per_key][:unless] = Array.wrap(options[:per_key][:unless])
|
151
|
-
end
|
152
|
-
|
153
|
-
def next_id
|
154
|
-
@@_callback_sequence += 1
|
155
|
-
end
|
156
|
-
|
157
|
-
def matches?(_kind, _filter)
|
158
|
-
@kind == _kind &&
|
159
|
-
@filter == _filter
|
160
|
-
end
|
161
|
-
|
162
|
-
def _update_filter(filter_options, new_options)
|
163
|
-
filter_options[:if].push(new_options[:unless]) if new_options.key?(:unless)
|
164
|
-
filter_options[:unless].push(new_options[:if]) if new_options.key?(:if)
|
165
|
-
end
|
166
|
-
|
167
|
-
def recompile!(_options, _per_key)
|
168
|
-
_update_filter(self.options, _options)
|
169
|
-
_update_filter(self.per_key, _per_key)
|
170
|
-
|
171
|
-
@callback_id = next_id
|
172
|
-
@filter = _compile_filter(@raw_filter)
|
173
|
-
@compiled_options = _compile_options(@options)
|
174
|
-
_compile_per_key_options
|
175
|
-
end
|
176
|
-
|
177
|
-
def _compile_per_key_options
|
178
|
-
key_options = _compile_options(@per_key)
|
179
|
-
|
180
|
-
@klass.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
|
181
|
-
def _one_time_conditions_valid_#{@callback_id}?
|
182
|
-
true #{key_options[0]}
|
183
|
-
end
|
184
|
-
RUBY_EVAL
|
185
|
-
end
|
186
|
-
|
187
|
-
# This will supply contents for before and around filters, and no
|
188
|
-
# contents for after filters (for the forward pass).
|
189
|
-
def start(key = nil, options = {})
|
190
|
-
object, terminator = (options || {}).values_at(:object, :terminator)
|
191
|
-
|
192
|
-
return if key && !object.send("_one_time_conditions_valid_#{@callback_id}?")
|
193
|
-
|
194
|
-
terminator ||= false
|
195
|
-
|
196
|
-
# options[0] is the compiled form of supplied conditions
|
197
|
-
# options[1] is the "end" for the conditional
|
198
|
-
|
199
|
-
if @kind == :before || @kind == :around
|
200
|
-
if @kind == :before
|
201
|
-
# if condition # before_save :filter_name, :if => :condition
|
202
|
-
# filter_name
|
203
|
-
# end
|
204
|
-
filter = <<-RUBY_EVAL
|
205
|
-
unless halted
|
206
|
-
result = #{@filter}
|
207
|
-
halted = (#{terminator})
|
208
|
-
end
|
209
|
-
RUBY_EVAL
|
210
|
-
|
211
|
-
[@compiled_options[0], filter, @compiled_options[1]].compact.join("\n")
|
212
|
-
else
|
213
|
-
# Compile around filters with conditions into proxy methods
|
214
|
-
# that contain the conditions.
|
215
|
-
#
|
216
|
-
# For `around_save :filter_name, :if => :condition':
|
217
|
-
#
|
218
|
-
# def _conditional_callback_save_17
|
219
|
-
# if condition
|
220
|
-
# filter_name do
|
221
|
-
# yield self
|
222
|
-
# end
|
223
|
-
# else
|
224
|
-
# yield self
|
225
|
-
# end
|
226
|
-
# end
|
227
|
-
|
228
|
-
name = "_conditional_callback_#{@kind}_#{next_id}"
|
229
|
-
txt, line = <<-RUBY_EVAL, __LINE__ + 1
|
230
|
-
def #{name}(halted)
|
231
|
-
#{@compiled_options[0] || "if true"} && !halted
|
232
|
-
#{@filter} do
|
233
|
-
yield self
|
234
|
-
end
|
235
|
-
else
|
236
|
-
yield self
|
237
|
-
end
|
238
|
-
end
|
239
|
-
RUBY_EVAL
|
240
|
-
@klass.class_eval(txt, __FILE__, line)
|
241
|
-
"#{name}(halted) do"
|
242
|
-
end
|
243
|
-
end
|
244
|
-
end
|
245
|
-
|
246
|
-
# This will supply contents for around and after filters, but not
|
247
|
-
# before filters (for the backward pass).
|
248
|
-
def end(key = nil, options = {})
|
249
|
-
object = (options || {})[:object]
|
250
|
-
|
251
|
-
return if key && !object.send("_one_time_conditions_valid_#{@callback_id}?")
|
252
|
-
|
253
|
-
if @kind == :around || @kind == :after
|
254
|
-
# if condition # after_save :filter_name, :if => :condition
|
255
|
-
# filter_name
|
256
|
-
# end
|
257
|
-
if @kind == :after
|
258
|
-
[@compiled_options[0], @filter, @compiled_options[1]].compact.join("\n")
|
259
|
-
else
|
260
|
-
"end"
|
261
|
-
end
|
262
|
-
end
|
263
|
-
end
|
264
|
-
|
265
|
-
private
|
266
|
-
# Options support the same options as filters themselves (and support
|
267
|
-
# symbols, string, procs, and objects), so compile a conditional
|
268
|
-
# expression based on the options
|
269
|
-
def _compile_options(options)
|
270
|
-
return [] if options[:if].empty? && options[:unless].empty?
|
271
|
-
|
272
|
-
conditions = []
|
273
|
-
|
274
|
-
unless options[:if].empty?
|
275
|
-
conditions << Array.wrap(_compile_filter(options[:if]))
|
276
|
-
end
|
277
|
-
|
278
|
-
unless options[:unless].empty?
|
279
|
-
conditions << Array.wrap(_compile_filter(options[:unless])).map {|f| "!#{f}"}
|
280
|
-
end
|
281
|
-
|
282
|
-
["if #{conditions.flatten.join(" && ")}", "end"]
|
283
|
-
end
|
284
|
-
|
285
|
-
# Filters support:
|
286
|
-
# Arrays:: Used in conditions. This is used to specify
|
287
|
-
# multiple conditions. Used internally to
|
288
|
-
# merge conditions from skip_* filters
|
289
|
-
# Symbols:: A method to call
|
290
|
-
# Strings:: Some content to evaluate
|
291
|
-
# Procs:: A proc to call with the object
|
292
|
-
# Objects:: An object with a before_foo method on it to call
|
293
|
-
#
|
294
|
-
# All of these objects are compiled into methods and handled
|
295
|
-
# the same after this point:
|
296
|
-
# Arrays:: Merged together into a single filter
|
297
|
-
# Symbols:: Already methods
|
298
|
-
# Strings:: class_eval'ed into methods
|
299
|
-
# Procs:: define_method'ed into methods
|
300
|
-
# Objects::
|
301
|
-
# a method is created that calls the before_foo method
|
302
|
-
# on the object.
|
303
|
-
def _compile_filter(filter)
|
304
|
-
method_name = "_callback_#{@kind}_#{next_id}"
|
305
|
-
case filter
|
306
|
-
when Array
|
307
|
-
filter.map {|f| _compile_filter(f)}
|
308
|
-
when Symbol
|
309
|
-
filter
|
310
|
-
when String
|
311
|
-
"(#{filter})"
|
312
|
-
when Proc
|
313
|
-
@klass.send(:define_method, method_name, &filter)
|
314
|
-
return method_name if filter.arity == 0
|
315
|
-
|
316
|
-
method_name << (filter.arity == 1 ? "(self)" : " self, Proc.new ")
|
317
|
-
else
|
318
|
-
@klass.send(:define_method, "#{method_name}_object") { filter }
|
319
|
-
|
320
|
-
_normalize_legacy_filter(kind, filter)
|
321
|
-
|
322
|
-
@klass.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
|
323
|
-
def #{method_name}(&blk)
|
324
|
-
#{method_name}_object.send(:#{kind}, self, &blk)
|
325
|
-
end
|
326
|
-
RUBY_EVAL
|
327
|
-
|
328
|
-
method_name
|
329
|
-
end
|
330
|
-
end
|
331
|
-
|
332
|
-
def _normalize_legacy_filter(kind, filter)
|
333
|
-
if !filter.respond_to?(kind) && filter.respond_to?(:filter)
|
334
|
-
filter.class_eval(
|
335
|
-
"def #{kind}(context, &block) filter(context, &block) end",
|
336
|
-
__FILE__, __LINE__ - 1)
|
337
|
-
elsif filter.respond_to?(:before) && filter.respond_to?(:after) && kind == :around
|
338
|
-
def filter.around(context)
|
339
|
-
should_continue = before(context)
|
340
|
-
yield if should_continue
|
341
|
-
after(context)
|
342
|
-
end
|
343
|
-
end
|
344
|
-
end
|
345
|
-
|
346
|
-
end
|
347
|
-
|
348
|
-
# An Array with a compile method
|
349
|
-
class CallbackChain < Array
|
350
|
-
def initialize(symbol)
|
351
|
-
@symbol = symbol
|
352
|
-
end
|
353
|
-
|
354
|
-
def compile(key = nil, options = {})
|
355
|
-
method = []
|
356
|
-
method << "halted = false"
|
357
|
-
each do |callback|
|
358
|
-
method << callback.start(key, options)
|
359
|
-
end
|
360
|
-
method << "yield self if block_given? && !halted"
|
361
|
-
reverse_each do |callback|
|
362
|
-
method << callback.end(key, options)
|
363
|
-
end
|
364
|
-
method.compact.join("\n")
|
365
|
-
end
|
366
|
-
|
367
|
-
def clone(klass)
|
368
|
-
chain = CallbackChain.new(@symbol)
|
369
|
-
chain.push(*map {|c| c.clone(klass)})
|
370
|
-
end
|
371
|
-
end
|
372
|
-
|
373
|
-
module ClassMethods
|
374
|
-
#CHAINS = {:before => :before, :around => :before, :after => :after}
|
375
|
-
|
376
|
-
# Make the _run_save_callbacks method. The generated method takes
|
377
|
-
# a block that it'll yield to. It'll call the before and around filters
|
378
|
-
# in order, yield the block, and then run the after filters.
|
379
|
-
#
|
380
|
-
# _run_save_callbacks do
|
381
|
-
# save
|
382
|
-
# end
|
383
|
-
#
|
384
|
-
# The _run_save_callbacks method can optionally take a key, which
|
385
|
-
# will be used to compile an optimized callback method for each
|
386
|
-
# key. See #define_callbacks for more information.
|
387
|
-
def _define_runner(symbol)
|
388
|
-
body = send("_#{symbol}_callback").
|
389
|
-
compile(nil, :terminator => send("_#{symbol}_terminator"))
|
390
|
-
|
391
|
-
body, line = <<-RUBY_EVAL, __LINE__ + 1
|
392
|
-
def _run_#{symbol}_callbacks(key = nil, &blk)
|
393
|
-
if key
|
394
|
-
name = "_run__\#{self.class.name.hash.abs}__#{symbol}__\#{key.hash.abs}__callbacks"
|
395
|
-
|
396
|
-
unless respond_to?(name)
|
397
|
-
self.class._create_keyed_callback(name, :#{symbol}, self, &blk)
|
398
|
-
end
|
399
|
-
|
400
|
-
send(name, &blk)
|
401
|
-
else
|
402
|
-
#{body}
|
403
|
-
end
|
404
|
-
end
|
405
|
-
RUBY_EVAL
|
406
|
-
|
407
|
-
undef_method "_run_#{symbol}_callbacks" if method_defined?("_run_#{symbol}_callbacks")
|
408
|
-
class_eval body, __FILE__, line
|
409
|
-
end
|
410
|
-
|
411
|
-
# This is called the first time a callback is called with a particular
|
412
|
-
# key. It creates a new callback method for the key, calculating
|
413
|
-
# which callbacks can be omitted because of per_key conditions.
|
414
|
-
def _create_keyed_callback(name, kind, obj, &blk)
|
415
|
-
@_keyed_callbacks ||= {}
|
416
|
-
@_keyed_callbacks[name] ||= begin
|
417
|
-
str = send("_#{kind}_callback").
|
418
|
-
compile(name, :object => obj, :terminator => send("_#{kind}_terminator"))
|
419
|
-
|
420
|
-
class_eval "def #{name}() #{str} end", __FILE__, __LINE__
|
421
|
-
|
422
|
-
true
|
423
|
-
end
|
424
|
-
end
|
425
|
-
|
426
|
-
# Define callbacks.
|
427
|
-
#
|
428
|
-
# Creates a <name>_callback method that you can use to add callbacks.
|
429
|
-
#
|
430
|
-
# Syntax:
|
431
|
-
# save_callback :before, :before_meth
|
432
|
-
# save_callback :after, :after_meth, :if => :condition
|
433
|
-
# save_callback :around {|r| stuff; yield; stuff }
|
434
|
-
#
|
435
|
-
# The <name>_callback method also updates the _run_<name>_callbacks
|
436
|
-
# method, which is the public API to run the callbacks.
|
437
|
-
#
|
438
|
-
# Also creates a skip_<name>_callback method that you can use to skip
|
439
|
-
# callbacks.
|
440
|
-
#
|
441
|
-
# When creating or skipping callbacks, you can specify conditions that
|
442
|
-
# are always the same for a given key. For instance, in ActionPack,
|
443
|
-
# we convert :only and :except conditions into per-key conditions.
|
444
|
-
#
|
445
|
-
# before_filter :authenticate, :except => "index"
|
446
|
-
# becomes
|
447
|
-
# dispatch_callback :before, :authenticate, :per_key => {:unless => proc {|c| c.action_name == "index"}}
|
448
|
-
#
|
449
|
-
# Per-Key conditions are evaluated only once per use of a given key.
|
450
|
-
# In the case of the above example, you would do:
|
451
|
-
#
|
452
|
-
# run_dispatch_callbacks(action_name) { ... dispatch stuff ... }
|
453
|
-
#
|
454
|
-
# In that case, each action_name would get its own compiled callback
|
455
|
-
# method that took into consideration the per_key conditions. This
|
456
|
-
# is a speed improvement for ActionPack.
|
457
|
-
def _update_callbacks(name, filters = CallbackChain.new(name), block = nil)
|
458
|
-
type = [:before, :after, :around].include?(filters.first) ? filters.shift : :before
|
459
|
-
options = filters.last.is_a?(Hash) ? filters.pop : {}
|
460
|
-
filters.unshift(block) if block
|
461
|
-
|
462
|
-
callbacks = send("_#{name}_callback")
|
463
|
-
yield callbacks, type, filters, options if block_given?
|
464
|
-
|
465
|
-
_define_runner(name)
|
466
|
-
end
|
467
|
-
|
468
|
-
alias_method :_reset_callbacks, :_update_callbacks
|
469
|
-
|
470
|
-
def set_callback(name, *filters, &block)
|
471
|
-
_update_callbacks(name, filters, block) do |callbacks, type, filters, options|
|
472
|
-
filters.map! do |filter|
|
473
|
-
# overrides parent class
|
474
|
-
callbacks.delete_if {|c| c.matches?(type, filter) }
|
475
|
-
Callback.new(filter, type, options.dup, self)
|
476
|
-
end
|
477
|
-
|
478
|
-
options[:prepend] ? callbacks.unshift(*filters) : callbacks.push(*filters)
|
479
|
-
end
|
480
|
-
end
|
481
|
-
|
482
|
-
def skip_callback(name, *filters, &block)
|
483
|
-
_update_callbacks(name, filters, block) do |callbacks, type, filters, options|
|
484
|
-
filters.each do |filter|
|
485
|
-
callbacks = send("_#{name}_callback=", callbacks.clone(self))
|
486
|
-
|
487
|
-
filter = callbacks.find {|c| c.matches?(type, filter) }
|
488
|
-
|
489
|
-
if filter && options.any?
|
490
|
-
filter.recompile!(options, options[:per_key] || {})
|
491
|
-
else
|
492
|
-
callbacks.delete(filter)
|
493
|
-
end
|
494
|
-
end
|
495
|
-
end
|
496
|
-
end
|
497
|
-
|
498
|
-
def define_callbacks(*symbols)
|
499
|
-
terminator = symbols.pop if symbols.last.is_a?(String)
|
500
|
-
symbols.each do |symbol|
|
501
|
-
extlib_inheritable_accessor("_#{symbol}_terminator") { terminator }
|
502
|
-
|
503
|
-
extlib_inheritable_accessor("_#{symbol}_callback") do
|
504
|
-
CallbackChain.new(symbol)
|
505
|
-
end
|
506
|
-
|
507
|
-
_define_runner(symbol)
|
508
|
-
|
509
|
-
# Define more convenient callback methods
|
510
|
-
# set_callback(:save, :before) becomes before_save
|
511
|
-
[:before, :after, :around].each do |filter|
|
512
|
-
self.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
|
513
|
-
def self.#{filter}_#{symbol}(*symbols, &blk)
|
514
|
-
_alias_callbacks(symbols, blk) do |callback, options|
|
515
|
-
set_callback(:#{symbol}, :#{filter}, callback, options)
|
516
|
-
end
|
517
|
-
end
|
518
|
-
RUBY_EVAL
|
519
|
-
end
|
520
|
-
end
|
521
|
-
end
|
522
|
-
|
523
|
-
def _alias_callbacks(callbacks, block)
|
524
|
-
options = callbacks.last.is_a?(Hash) ? callbacks.pop : {}
|
525
|
-
callbacks.push(block) if block
|
526
|
-
callbacks.each do |callback|
|
527
|
-
yield callback, options
|
528
|
-
end
|
529
|
-
end
|
530
|
-
end
|
531
|
-
end
|
532
|
-
end
|