cassandra_mapper 0.0.1
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.rdoc +98 -0
- data/Rakefile.rb +11 -0
- data/lib/cassandra_mapper.rb +5 -0
- data/lib/cassandra_mapper/base.rb +19 -0
- data/lib/cassandra_mapper/connection.rb +9 -0
- data/lib/cassandra_mapper/core_ext/array/extract_options.rb +29 -0
- data/lib/cassandra_mapper/core_ext/array/wrap.rb +22 -0
- data/lib/cassandra_mapper/core_ext/class/inheritable_attributes.rb +232 -0
- data/lib/cassandra_mapper/core_ext/kernel/reporting.rb +62 -0
- data/lib/cassandra_mapper/core_ext/kernel/singleton_class.rb +13 -0
- data/lib/cassandra_mapper/core_ext/module/aliasing.rb +70 -0
- data/lib/cassandra_mapper/core_ext/module/attribute_accessors.rb +66 -0
- data/lib/cassandra_mapper/core_ext/object/duplicable.rb +65 -0
- data/lib/cassandra_mapper/core_ext/string/inflections.rb +160 -0
- data/lib/cassandra_mapper/core_ext/string/multibyte.rb +72 -0
- data/lib/cassandra_mapper/exceptions.rb +10 -0
- data/lib/cassandra_mapper/identity.rb +29 -0
- data/lib/cassandra_mapper/indexing.rb +465 -0
- data/lib/cassandra_mapper/observable.rb +36 -0
- data/lib/cassandra_mapper/persistence.rb +309 -0
- data/lib/cassandra_mapper/support/callbacks.rb +136 -0
- data/lib/cassandra_mapper/support/concern.rb +31 -0
- data/lib/cassandra_mapper/support/dependencies.rb +60 -0
- data/lib/cassandra_mapper/support/descendants_tracker.rb +41 -0
- data/lib/cassandra_mapper/support/inflections.rb +58 -0
- data/lib/cassandra_mapper/support/inflector.rb +7 -0
- data/lib/cassandra_mapper/support/inflector/inflections.rb +213 -0
- data/lib/cassandra_mapper/support/inflector/methods.rb +143 -0
- data/lib/cassandra_mapper/support/inflector/transliterate.rb +99 -0
- data/lib/cassandra_mapper/support/multibyte.rb +46 -0
- data/lib/cassandra_mapper/support/multibyte/utils.rb +62 -0
- data/lib/cassandra_mapper/support/observing.rb +218 -0
- data/lib/cassandra_mapper/support/support_callbacks.rb +593 -0
- data/test/test_helper.rb +11 -0
- data/test/unit/callbacks_test.rb +100 -0
- data/test/unit/identity_test.rb +51 -0
- data/test/unit/indexing_test.rb +406 -0
- data/test/unit/observer_test.rb +56 -0
- data/test/unit/persistence_test.rb +561 -0
- metadata +192 -0
@@ -0,0 +1,99 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'cassandra_mapper/core_ext/string/multibyte'
|
3
|
+
|
4
|
+
module CassandraMapper
|
5
|
+
module Support
|
6
|
+
module Inflector
|
7
|
+
|
8
|
+
# Replaces non-ASCII characters with an ASCII approximation, or if none
|
9
|
+
# exists, a replacement character which defaults to "?".
|
10
|
+
#
|
11
|
+
# transliterate("Ærøskøbing")
|
12
|
+
# # => "AEroskobing"
|
13
|
+
#
|
14
|
+
# Default approximations are provided for Western/Latin characters,
|
15
|
+
# e.g, "ø", "ñ", "é", "ß", etc.
|
16
|
+
#
|
17
|
+
# This method is I18n aware, so you can set up custom approximations for a
|
18
|
+
# locale. This can be useful, for example, to transliterate German's "ü"
|
19
|
+
# and "ö" to "ue" and "oe", or to add support for transliterating Russian
|
20
|
+
# to ASCII.
|
21
|
+
#
|
22
|
+
# In order to make your custom transliterations available, you must set
|
23
|
+
# them as the <tt>i18n.transliterate.rule</tt> i18n key:
|
24
|
+
#
|
25
|
+
# # Store the transliterations in locales/de.yml
|
26
|
+
# i18n:
|
27
|
+
# transliterate:
|
28
|
+
# rule:
|
29
|
+
# ü: "ue"
|
30
|
+
# ö: "oe"
|
31
|
+
#
|
32
|
+
# # Or set them using Ruby
|
33
|
+
# I18n.backend.store_translations(:de, :i18n => {
|
34
|
+
# :transliterate => {
|
35
|
+
# :rule => {
|
36
|
+
# "ü" => "ue",
|
37
|
+
# "ö" => "oe"
|
38
|
+
# }
|
39
|
+
# }
|
40
|
+
# })
|
41
|
+
#
|
42
|
+
# The value for <tt>i18n.transliterate.rule</tt> can be a simple Hash that maps
|
43
|
+
# characters to ASCII approximations as shown above, or, for more complex
|
44
|
+
# requirements, a Proc:
|
45
|
+
#
|
46
|
+
# I18n.backend.store_translations(:de, :i18n => {
|
47
|
+
# :transliterate => {
|
48
|
+
# :rule => lambda {|string| MyTransliterator.transliterate(string)}
|
49
|
+
# }
|
50
|
+
# })
|
51
|
+
#
|
52
|
+
# Now you can have different transliterations for each locale:
|
53
|
+
#
|
54
|
+
# I18n.locale = :en
|
55
|
+
# transliterate("Jürgen")
|
56
|
+
# # => "Jurgen"
|
57
|
+
#
|
58
|
+
# I18n.locale = :de
|
59
|
+
# transliterate("Jürgen")
|
60
|
+
# # => "Juergen"
|
61
|
+
def transliterate(string, replacement = "?")
|
62
|
+
I18n.transliterate(ActiveSupport::Multibyte::Unicode.normalize(
|
63
|
+
ActiveSupport::Multibyte::Unicode.tidy_bytes(string), :c),
|
64
|
+
:replacement => replacement)
|
65
|
+
end
|
66
|
+
|
67
|
+
# Replaces special characters in a string so that it may be used as part of a 'pretty' URL.
|
68
|
+
#
|
69
|
+
# ==== Examples
|
70
|
+
#
|
71
|
+
# class Person
|
72
|
+
# def to_param
|
73
|
+
# "#{id}-#{name.parameterize}"
|
74
|
+
# end
|
75
|
+
# end
|
76
|
+
#
|
77
|
+
# @person = Person.find(1)
|
78
|
+
# # => #<Person id: 1, name: "Donald E. Knuth">
|
79
|
+
#
|
80
|
+
# <%= link_to(@person.name, person_path(@person)) %>
|
81
|
+
# # => <a href="/person/1-donald-e-knuth">Donald E. Knuth</a>
|
82
|
+
def parameterize(string, sep = '-')
|
83
|
+
# replace accented chars with their ascii equivalents
|
84
|
+
parameterized_string = transliterate(string)
|
85
|
+
# Turn unwanted chars into the separator
|
86
|
+
parameterized_string.gsub!(/[^a-z0-9\-_]+/i, sep)
|
87
|
+
unless sep.nil? || sep.empty?
|
88
|
+
re_sep = Regexp.escape(sep)
|
89
|
+
# No more than one of the separator in a row.
|
90
|
+
parameterized_string.gsub!(/#{re_sep}{2,}/, sep)
|
91
|
+
# Remove leading/trailing separator.
|
92
|
+
parameterized_string.gsub!(/^#{re_sep}|#{re_sep}$/i, '')
|
93
|
+
end
|
94
|
+
parameterized_string.downcase
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'cassandra_mapper/core_ext/module/attribute_accessors'
|
3
|
+
|
4
|
+
module CassandraMapper #:nodoc:
|
5
|
+
module Support
|
6
|
+
module Multibyte
|
7
|
+
autoload :EncodingError, 'active_support/multibyte/exceptions'
|
8
|
+
autoload :Chars, 'active_support/multibyte/chars'
|
9
|
+
autoload :Unicode, 'active_support/multibyte/unicode'
|
10
|
+
|
11
|
+
# The proxy class returned when calling mb_chars. You can use this accessor to configure your own proxy
|
12
|
+
# class so you can support other encodings. See the ActiveSupport::Multibyte::Chars implementation for
|
13
|
+
# an example how to do this.
|
14
|
+
#
|
15
|
+
# Example:
|
16
|
+
# ActiveSupport::Multibyte.proxy_class = CharsForUTF32
|
17
|
+
def self.proxy_class=(klass)
|
18
|
+
@proxy_class = klass
|
19
|
+
end
|
20
|
+
|
21
|
+
# Returns the current proxy class
|
22
|
+
def self.proxy_class
|
23
|
+
@proxy_class ||= ActiveSupport::Multibyte::Chars
|
24
|
+
end
|
25
|
+
|
26
|
+
# Regular expressions that describe valid byte sequences for a character
|
27
|
+
VALID_CHARACTER = {
|
28
|
+
# Borrowed from the Kconv library by Shinji KONO - (also as seen on the W3C site)
|
29
|
+
'UTF-8' => /\A(?:
|
30
|
+
[\x00-\x7f] |
|
31
|
+
[\xc2-\xdf] [\x80-\xbf] |
|
32
|
+
\xe0 [\xa0-\xbf] [\x80-\xbf] |
|
33
|
+
[\xe1-\xef] [\x80-\xbf] [\x80-\xbf] |
|
34
|
+
\xf0 [\x90-\xbf] [\x80-\xbf] [\x80-\xbf] |
|
35
|
+
[\xf1-\xf3] [\x80-\xbf] [\x80-\xbf] [\x80-\xbf] |
|
36
|
+
\xf4 [\x80-\x8f] [\x80-\xbf] [\x80-\xbf])\z /xn,
|
37
|
+
# Quick check for valid Shift-JIS characters, disregards the odd-even pairing
|
38
|
+
'Shift_JIS' => /\A(?:
|
39
|
+
[\x00-\x7e\xa1-\xdf] |
|
40
|
+
[\x81-\x9f\xe0-\xef] [\x40-\x7e\x80-\x9e\x9f-\xfc])\z /xn
|
41
|
+
}
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
require 'cassandra_mapper/support/multibyte/utils'
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module CassandraMapper #:nodoc:
|
4
|
+
module Support
|
5
|
+
module Multibyte #:nodoc:
|
6
|
+
if Kernel.const_defined?(:Encoding)
|
7
|
+
# Returns a regular expression that matches valid characters in the current encoding
|
8
|
+
def self.valid_character
|
9
|
+
VALID_CHARACTER[Encoding.default_external.to_s]
|
10
|
+
end
|
11
|
+
else
|
12
|
+
def self.valid_character
|
13
|
+
case $KCODE
|
14
|
+
when 'UTF8'
|
15
|
+
VALID_CHARACTER['UTF-8']
|
16
|
+
when 'SJIS'
|
17
|
+
VALID_CHARACTER['Shift_JIS']
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
if 'string'.respond_to?(:valid_encoding?)
|
23
|
+
# Verifies the encoding of a string
|
24
|
+
def self.verify(string)
|
25
|
+
string.valid_encoding?
|
26
|
+
end
|
27
|
+
else
|
28
|
+
def self.verify(string)
|
29
|
+
if expression = valid_character
|
30
|
+
# Splits the string on character boundaries, which are determined based on $KCODE.
|
31
|
+
string.split(//).all? { |c| expression =~ c }
|
32
|
+
else
|
33
|
+
true
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# Verifies the encoding of the string and raises an exception when it's not valid
|
39
|
+
def self.verify!(string)
|
40
|
+
raise EncodingError.new("Found characters with invalid encoding") unless verify(string)
|
41
|
+
end
|
42
|
+
|
43
|
+
if 'string'.respond_to?(:force_encoding)
|
44
|
+
# Removes all invalid characters from the string.
|
45
|
+
#
|
46
|
+
# Note: this method is a no-op in Ruby 1.9
|
47
|
+
def self.clean(string)
|
48
|
+
string
|
49
|
+
end
|
50
|
+
else
|
51
|
+
def self.clean(string)
|
52
|
+
if expression = valid_character
|
53
|
+
# Splits the string on character boundaries, which are determined based on $KCODE.
|
54
|
+
string.split(//).grep(expression).join
|
55
|
+
else
|
56
|
+
string
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,218 @@
|
|
1
|
+
require 'singleton'
|
2
|
+
require 'cassandra_mapper/support/concern'
|
3
|
+
require 'cassandra_mapper/core_ext/array/wrap'
|
4
|
+
require 'cassandra_mapper/core_ext/module/aliasing'
|
5
|
+
require 'cassandra_mapper/core_ext/string/inflections'
|
6
|
+
|
7
|
+
module CassandraMapper
|
8
|
+
module Support
|
9
|
+
module Observing
|
10
|
+
extend CassandraMapper::Support::Concern
|
11
|
+
|
12
|
+
module ClassMethods
|
13
|
+
# == Active Model Observers Activation
|
14
|
+
#
|
15
|
+
# Activates the observers assigned. Examples:
|
16
|
+
#
|
17
|
+
# # Calls PersonObserver.instance
|
18
|
+
# ActiveRecord::Base.observers = :person_observer
|
19
|
+
#
|
20
|
+
# # Calls Cacher.instance and GarbageCollector.instance
|
21
|
+
# ActiveRecord::Base.observers = :cacher, :garbage_collector
|
22
|
+
#
|
23
|
+
# # Same as above, just using explicit class references
|
24
|
+
# ActiveRecord::Base.observers = Cacher, GarbageCollector
|
25
|
+
#
|
26
|
+
# Note: Setting this does not instantiate the observers yet.
|
27
|
+
# +instantiate_observers+ is called during startup, and before
|
28
|
+
# each development request.
|
29
|
+
def observers=(*values)
|
30
|
+
@observers = values.flatten
|
31
|
+
end
|
32
|
+
|
33
|
+
# Gets the current observers.
|
34
|
+
def observers
|
35
|
+
@observers ||= []
|
36
|
+
end
|
37
|
+
|
38
|
+
# Instantiate the global Active Record observers.
|
39
|
+
def instantiate_observers
|
40
|
+
observers.each { |o| instantiate_observer(o) }
|
41
|
+
end
|
42
|
+
|
43
|
+
def add_observer(observer)
|
44
|
+
unless observer.respond_to? :update
|
45
|
+
raise ArgumentError, "observer needs to respond to `update'"
|
46
|
+
end
|
47
|
+
@observer_instances ||= []
|
48
|
+
@observer_instances << observer
|
49
|
+
end
|
50
|
+
|
51
|
+
def notify_observers(*arg)
|
52
|
+
if defined? @observer_instances
|
53
|
+
for observer in @observer_instances
|
54
|
+
observer.update(*arg)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def count_observers
|
60
|
+
@observer_instances.size
|
61
|
+
end
|
62
|
+
|
63
|
+
protected
|
64
|
+
|
65
|
+
def instantiate_observer(observer) #:nodoc:
|
66
|
+
# string/symbol
|
67
|
+
if observer.respond_to?(:to_sym)
|
68
|
+
observer = observer.to_s.camelize.constantize.instance
|
69
|
+
elsif observer.respond_to?(:instance)
|
70
|
+
observer.instance
|
71
|
+
else
|
72
|
+
raise ArgumentError, "#{observer} must be a lowercase, underscored class name (or an instance of the class itself) responding to the instance method. Example: Person.observers = :big_brother # calls BigBrother.instance"
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# Notify observers when the observed class is subclassed.
|
77
|
+
def inherited(subclass)
|
78
|
+
super
|
79
|
+
notify_observers :observed_class_inherited, subclass
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
# Fires notifications to model's observers
|
85
|
+
#
|
86
|
+
# def save
|
87
|
+
# notify_observers(:before_save)
|
88
|
+
# ...
|
89
|
+
# notify_observers(:after_save)
|
90
|
+
# end
|
91
|
+
def notify_observers(method)
|
92
|
+
self.class.notify_observers(method, self)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# == Active Model Observers
|
97
|
+
#
|
98
|
+
# Observer classes respond to lifecycle callbacks to implement trigger-like
|
99
|
+
# behavior outside the original class. This is a great way to reduce the
|
100
|
+
# clutter that normally comes when the model class is burdened with
|
101
|
+
# functionality that doesn't pertain to the core responsibility of the
|
102
|
+
# class. Example:
|
103
|
+
#
|
104
|
+
# class CommentObserver < ActiveModel::Observer
|
105
|
+
# def after_save(comment)
|
106
|
+
# Notifications.deliver_comment("admin@do.com", "New comment was posted", comment)
|
107
|
+
# end
|
108
|
+
# end
|
109
|
+
#
|
110
|
+
# This Observer sends an email when a Comment#save is finished.
|
111
|
+
#
|
112
|
+
# class ContactObserver < ActiveModel::Observer
|
113
|
+
# def after_create(contact)
|
114
|
+
# contact.logger.info('New contact added!')
|
115
|
+
# end
|
116
|
+
#
|
117
|
+
# def after_destroy(contact)
|
118
|
+
# contact.logger.warn("Contact with an id of #{contact.id} was destroyed!")
|
119
|
+
# end
|
120
|
+
# end
|
121
|
+
#
|
122
|
+
# This Observer uses logger to log when specific callbacks are triggered.
|
123
|
+
#
|
124
|
+
# == Observing a class that can't be inferred
|
125
|
+
#
|
126
|
+
# Observers will by default be mapped to the class with which they share a
|
127
|
+
# name. So CommentObserver will be tied to observing Comment, ProductManagerObserver
|
128
|
+
# to ProductManager, and so on. If you want to name your observer differently than
|
129
|
+
# the class you're interested in observing, you can use the Observer.observe class
|
130
|
+
# method which takes either the concrete class (Product) or a symbol for that
|
131
|
+
# class (:product):
|
132
|
+
#
|
133
|
+
# class AuditObserver < ActiveModel::Observer
|
134
|
+
# observe :account
|
135
|
+
#
|
136
|
+
# def after_update(account)
|
137
|
+
# AuditTrail.new(account, "UPDATED")
|
138
|
+
# end
|
139
|
+
# end
|
140
|
+
#
|
141
|
+
# If the audit observer needs to watch more than one kind of object, this can be
|
142
|
+
# specified with multiple arguments:
|
143
|
+
#
|
144
|
+
# class AuditObserver < ActiveModel::Observer
|
145
|
+
# observe :account, :balance
|
146
|
+
#
|
147
|
+
# def after_update(record)
|
148
|
+
# AuditTrail.new(record, "UPDATED")
|
149
|
+
# end
|
150
|
+
# end
|
151
|
+
#
|
152
|
+
# The AuditObserver will now act on both updates to Account and Balance by treating
|
153
|
+
# them both as records.
|
154
|
+
#
|
155
|
+
class Observer
|
156
|
+
include Singleton
|
157
|
+
|
158
|
+
class << self
|
159
|
+
# Attaches the observer to the supplied model classes.
|
160
|
+
def observe(*models)
|
161
|
+
models.flatten!
|
162
|
+
models.collect! { |model| model.respond_to?(:to_sym) ? model.to_s.camelize.constantize : model }
|
163
|
+
define_method(:observed_classes) { models }
|
164
|
+
end
|
165
|
+
|
166
|
+
# Returns an array of Classes to observe.
|
167
|
+
#
|
168
|
+
# You can override this instead of using the +observe+ helper.
|
169
|
+
#
|
170
|
+
# class AuditObserver < ActiveModel::Observer
|
171
|
+
# def self.observed_classes
|
172
|
+
# [Account, Balance]
|
173
|
+
# end
|
174
|
+
# end
|
175
|
+
def observed_classes
|
176
|
+
Array.wrap(observed_class)
|
177
|
+
end
|
178
|
+
|
179
|
+
# The class observed by default is inferred from the observer's class name:
|
180
|
+
# assert_equal Person, PersonObserver.observed_class
|
181
|
+
def observed_class
|
182
|
+
if observed_class_name = name[/(.*)Observer/, 1]
|
183
|
+
observed_class_name.constantize
|
184
|
+
else
|
185
|
+
nil
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
# Start observing the declared classes and their subclasses.
|
191
|
+
def initialize
|
192
|
+
observed_classes.each { |klass| add_observer!(klass) }
|
193
|
+
end
|
194
|
+
|
195
|
+
def observed_classes #:nodoc:
|
196
|
+
self.class.observed_classes
|
197
|
+
end
|
198
|
+
|
199
|
+
# Send observed_method(object) if the method exists.
|
200
|
+
def update(observed_method, object) #:nodoc:
|
201
|
+
send(observed_method, object) if respond_to?(observed_method)
|
202
|
+
end
|
203
|
+
|
204
|
+
# Special method sent by the observed class when it is inherited.
|
205
|
+
# Passes the new subclass.
|
206
|
+
def observed_class_inherited(subclass) #:nodoc:
|
207
|
+
self.class.observe(observed_classes + [subclass])
|
208
|
+
add_observer!(subclass)
|
209
|
+
end
|
210
|
+
|
211
|
+
protected
|
212
|
+
|
213
|
+
def add_observer!(klass) #:nodoc:
|
214
|
+
klass.add_observer(self)
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
@@ -0,0 +1,593 @@
|
|
1
|
+
require 'cassandra_mapper/support/descendants_tracker'
|
2
|
+
require 'cassandra_mapper/support/concern'
|
3
|
+
require 'cassandra_mapper/core_ext/array/wrap'
|
4
|
+
require 'cassandra_mapper/core_ext/class/inheritable_attributes'
|
5
|
+
require 'cassandra_mapper/core_ext/kernel/reporting'
|
6
|
+
require 'cassandra_mapper/core_ext/kernel/singleton_class'
|
7
|
+
|
8
|
+
module CassandraMapper
|
9
|
+
module Support
|
10
|
+
# Callbacks are hooks into the lifecycle of an object that allow you to trigger logic
|
11
|
+
# before or after an alteration of the object state.
|
12
|
+
#
|
13
|
+
# Mixing in this module allows you to define callbacks in your class.
|
14
|
+
#
|
15
|
+
# Example:
|
16
|
+
# class Storage
|
17
|
+
# include ActiveSupport::Callbacks
|
18
|
+
#
|
19
|
+
# define_callbacks :save
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# class ConfigStorage < Storage
|
23
|
+
# set_callback :save, :before, :saving_message
|
24
|
+
# def saving_message
|
25
|
+
# puts "saving..."
|
26
|
+
# end
|
27
|
+
#
|
28
|
+
# set_callback :save, :after do |object|
|
29
|
+
# puts "saved"
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
# def save
|
33
|
+
# run_callbacks :save do
|
34
|
+
# puts "- save"
|
35
|
+
# end
|
36
|
+
# end
|
37
|
+
# end
|
38
|
+
#
|
39
|
+
# config = ConfigStorage.new
|
40
|
+
# config.save
|
41
|
+
#
|
42
|
+
# Output:
|
43
|
+
# saving...
|
44
|
+
# - save
|
45
|
+
# saved
|
46
|
+
#
|
47
|
+
# Callbacks from parent classes are inherited.
|
48
|
+
#
|
49
|
+
# Example:
|
50
|
+
# class Storage
|
51
|
+
# include ActiveSupport::Callbacks
|
52
|
+
#
|
53
|
+
# define_callbacks :save
|
54
|
+
#
|
55
|
+
# set_callback :save, :before, :prepare
|
56
|
+
# def prepare
|
57
|
+
# puts "preparing save"
|
58
|
+
# end
|
59
|
+
# end
|
60
|
+
#
|
61
|
+
# class ConfigStorage < Storage
|
62
|
+
# set_callback :save, :before, :saving_message
|
63
|
+
# def saving_message
|
64
|
+
# puts "saving..."
|
65
|
+
# end
|
66
|
+
#
|
67
|
+
# set_callback :save, :after do |object|
|
68
|
+
# puts "saved"
|
69
|
+
# end
|
70
|
+
#
|
71
|
+
# def save
|
72
|
+
# run_callbacks :save do
|
73
|
+
# puts "- save"
|
74
|
+
# end
|
75
|
+
# end
|
76
|
+
# end
|
77
|
+
#
|
78
|
+
# config = ConfigStorage.new
|
79
|
+
# config.save
|
80
|
+
#
|
81
|
+
# Output:
|
82
|
+
# preparing save
|
83
|
+
# saving...
|
84
|
+
# - save
|
85
|
+
# saved
|
86
|
+
#
|
87
|
+
module SupportCallbacks
|
88
|
+
extend CassandraMapper::Support::Concern
|
89
|
+
|
90
|
+
included do
|
91
|
+
extend CassandraMapper::Support::DescendantsTracker
|
92
|
+
end
|
93
|
+
|
94
|
+
def run_callbacks(kind, *args, &block)
|
95
|
+
send("_run_#{kind}_callbacks", *args, &block)
|
96
|
+
end
|
97
|
+
|
98
|
+
class Callback
|
99
|
+
@@_callback_sequence = 0
|
100
|
+
|
101
|
+
attr_accessor :chain, :filter, :kind, :options, :per_key, :klass, :raw_filter
|
102
|
+
|
103
|
+
def initialize(chain, filter, kind, options, klass)
|
104
|
+
@chain, @kind, @klass = chain, kind, klass
|
105
|
+
normalize_options!(options)
|
106
|
+
|
107
|
+
@per_key = options.delete(:per_key)
|
108
|
+
@raw_filter, @options = filter, options
|
109
|
+
@filter = _compile_filter(filter)
|
110
|
+
@compiled_options = _compile_options(options)
|
111
|
+
@callback_id = next_id
|
112
|
+
|
113
|
+
_compile_per_key_options
|
114
|
+
end
|
115
|
+
|
116
|
+
def clone(chain, klass)
|
117
|
+
obj = super()
|
118
|
+
obj.chain = chain
|
119
|
+
obj.klass = klass
|
120
|
+
obj.per_key = @per_key.dup
|
121
|
+
obj.options = @options.dup
|
122
|
+
obj.per_key[:if] = @per_key[:if].dup
|
123
|
+
obj.per_key[:unless] = @per_key[:unless].dup
|
124
|
+
obj.options[:if] = @options[:if].dup
|
125
|
+
obj.options[:unless] = @options[:unless].dup
|
126
|
+
obj
|
127
|
+
end
|
128
|
+
|
129
|
+
def normalize_options!(options)
|
130
|
+
options[:if] = Array.wrap(options[:if])
|
131
|
+
options[:unless] = Array.wrap(options[:unless])
|
132
|
+
|
133
|
+
options[:per_key] ||= {}
|
134
|
+
options[:per_key][:if] = Array.wrap(options[:per_key][:if])
|
135
|
+
options[:per_key][:unless] = Array.wrap(options[:per_key][:unless])
|
136
|
+
end
|
137
|
+
|
138
|
+
def name
|
139
|
+
chain.name
|
140
|
+
end
|
141
|
+
|
142
|
+
def next_id
|
143
|
+
@@_callback_sequence += 1
|
144
|
+
end
|
145
|
+
|
146
|
+
def matches?(_kind, _filter)
|
147
|
+
@kind == _kind && @filter == _filter
|
148
|
+
end
|
149
|
+
|
150
|
+
def _update_filter(filter_options, new_options)
|
151
|
+
filter_options[:if].push(new_options[:unless]) if new_options.key?(:unless)
|
152
|
+
filter_options[:unless].push(new_options[:if]) if new_options.key?(:if)
|
153
|
+
end
|
154
|
+
|
155
|
+
def recompile!(_options, _per_key)
|
156
|
+
_update_filter(self.options, _options)
|
157
|
+
_update_filter(self.per_key, _per_key)
|
158
|
+
|
159
|
+
@callback_id = next_id
|
160
|
+
@filter = _compile_filter(@raw_filter)
|
161
|
+
@compiled_options = _compile_options(@options)
|
162
|
+
_compile_per_key_options
|
163
|
+
end
|
164
|
+
|
165
|
+
def _compile_per_key_options
|
166
|
+
key_options = _compile_options(@per_key)
|
167
|
+
|
168
|
+
@klass.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
|
169
|
+
def _one_time_conditions_valid_#{@callback_id}?
|
170
|
+
true #{key_options[0]}
|
171
|
+
end
|
172
|
+
RUBY_EVAL
|
173
|
+
end
|
174
|
+
|
175
|
+
# This will supply contents for before and around filters, and no
|
176
|
+
# contents for after filters (for the forward pass).
|
177
|
+
def start(key=nil, object=nil)
|
178
|
+
return if key && !object.send("_one_time_conditions_valid_#{@callback_id}?")
|
179
|
+
|
180
|
+
# options[0] is the compiled form of supplied conditions
|
181
|
+
# options[1] is the "end" for the conditional
|
182
|
+
#
|
183
|
+
if @kind == :before || @kind == :around
|
184
|
+
if @kind == :before
|
185
|
+
# if condition # before_save :filter_name, :if => :condition
|
186
|
+
# filter_name
|
187
|
+
# end
|
188
|
+
filter = <<-RUBY_EVAL
|
189
|
+
unless halted
|
190
|
+
result = #{@filter}
|
191
|
+
halted = (#{chain.config[:terminator]})
|
192
|
+
end
|
193
|
+
RUBY_EVAL
|
194
|
+
|
195
|
+
[@compiled_options[0], filter, @compiled_options[1]].compact.join("\n")
|
196
|
+
else
|
197
|
+
# Compile around filters with conditions into proxy methods
|
198
|
+
# that contain the conditions.
|
199
|
+
#
|
200
|
+
# For `around_save :filter_name, :if => :condition':
|
201
|
+
#
|
202
|
+
# def _conditional_callback_save_17
|
203
|
+
# if condition
|
204
|
+
# filter_name do
|
205
|
+
# yield self
|
206
|
+
# end
|
207
|
+
# else
|
208
|
+
# yield self
|
209
|
+
# end
|
210
|
+
# end
|
211
|
+
#
|
212
|
+
name = "_conditional_callback_#{@kind}_#{next_id}"
|
213
|
+
@klass.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
|
214
|
+
def #{name}(halted)
|
215
|
+
#{@compiled_options[0] || "if true"} && !halted
|
216
|
+
#{@filter} do
|
217
|
+
yield self
|
218
|
+
end
|
219
|
+
else
|
220
|
+
yield self
|
221
|
+
end
|
222
|
+
end
|
223
|
+
RUBY_EVAL
|
224
|
+
"#{name}(halted) do"
|
225
|
+
end
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
# This will supply contents for around and after filters, but not
|
230
|
+
# before filters (for the backward pass).
|
231
|
+
def end(key=nil, object=nil)
|
232
|
+
return if key && !object.send("_one_time_conditions_valid_#{@callback_id}?")
|
233
|
+
|
234
|
+
if @kind == :around || @kind == :after
|
235
|
+
# if condition # after_save :filter_name, :if => :condition
|
236
|
+
# filter_name
|
237
|
+
# end
|
238
|
+
if @kind == :after
|
239
|
+
[@compiled_options[0], @filter, @compiled_options[1]].compact.join("\n")
|
240
|
+
else
|
241
|
+
"end"
|
242
|
+
end
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
private
|
247
|
+
|
248
|
+
# Options support the same options as filters themselves (and support
|
249
|
+
# symbols, string, procs, and objects), so compile a conditional
|
250
|
+
# expression based on the options
|
251
|
+
def _compile_options(options)
|
252
|
+
return [] if options[:if].empty? && options[:unless].empty?
|
253
|
+
|
254
|
+
conditions = []
|
255
|
+
|
256
|
+
unless options[:if].empty?
|
257
|
+
conditions << Array.wrap(_compile_filter(options[:if]))
|
258
|
+
end
|
259
|
+
|
260
|
+
unless options[:unless].empty?
|
261
|
+
conditions << Array.wrap(_compile_filter(options[:unless])).map {|f| "!#{f}"}
|
262
|
+
end
|
263
|
+
|
264
|
+
["if #{conditions.flatten.join(" && ")}", "end"]
|
265
|
+
end
|
266
|
+
|
267
|
+
# Filters support:
|
268
|
+
#
|
269
|
+
# Arrays:: Used in conditions. This is used to specify
|
270
|
+
# multiple conditions. Used internally to
|
271
|
+
# merge conditions from skip_* filters
|
272
|
+
# Symbols:: A method to call
|
273
|
+
# Strings:: Some content to evaluate
|
274
|
+
# Procs:: A proc to call with the object
|
275
|
+
# Objects:: An object with a before_foo method on it to call
|
276
|
+
#
|
277
|
+
# All of these objects are compiled into methods and handled
|
278
|
+
# the same after this point:
|
279
|
+
#
|
280
|
+
# Arrays:: Merged together into a single filter
|
281
|
+
# Symbols:: Already methods
|
282
|
+
# Strings:: class_eval'ed into methods
|
283
|
+
# Procs:: define_method'ed into methods
|
284
|
+
# Objects::
|
285
|
+
# a method is created that calls the before_foo method
|
286
|
+
# on the object.
|
287
|
+
#
|
288
|
+
def _compile_filter(filter)
|
289
|
+
method_name = "_callback_#{@kind}_#{next_id}"
|
290
|
+
case filter
|
291
|
+
when Array
|
292
|
+
filter.map {|f| _compile_filter(f)}
|
293
|
+
when Symbol
|
294
|
+
filter
|
295
|
+
when String
|
296
|
+
"(#{filter})"
|
297
|
+
when Proc
|
298
|
+
@klass.send(:define_method, method_name, &filter)
|
299
|
+
return method_name if filter.arity <= 0
|
300
|
+
|
301
|
+
method_name << (filter.arity == 1 ? "(self)" : " self, Proc.new ")
|
302
|
+
else
|
303
|
+
@klass.send(:define_method, "#{method_name}_object") { filter }
|
304
|
+
|
305
|
+
_normalize_legacy_filter(kind, filter)
|
306
|
+
scopes = Array.wrap(chain.config[:scope])
|
307
|
+
method_to_call = scopes.map{ |s| s.is_a?(Symbol) ? send(s) : s }.join("_")
|
308
|
+
|
309
|
+
@klass.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
|
310
|
+
def #{method_name}(&blk)
|
311
|
+
#{method_name}_object.send(:#{method_to_call}, self, &blk)
|
312
|
+
end
|
313
|
+
RUBY_EVAL
|
314
|
+
|
315
|
+
method_name
|
316
|
+
end
|
317
|
+
end
|
318
|
+
|
319
|
+
def _normalize_legacy_filter(kind, filter)
|
320
|
+
if !filter.respond_to?(kind) && filter.respond_to?(:filter)
|
321
|
+
filter.singleton_class.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
|
322
|
+
def #{kind}(context, &block) filter(context, &block) end
|
323
|
+
RUBY_EVAL
|
324
|
+
elsif filter.respond_to?(:before) && filter.respond_to?(:after) && kind == :around
|
325
|
+
def filter.around(context)
|
326
|
+
should_continue = before(context)
|
327
|
+
yield if should_continue
|
328
|
+
after(context)
|
329
|
+
end
|
330
|
+
end
|
331
|
+
end
|
332
|
+
end
|
333
|
+
|
334
|
+
# An Array with a compile method
|
335
|
+
class CallbackChain < Array
|
336
|
+
attr_reader :name, :config
|
337
|
+
|
338
|
+
def initialize(name, config)
|
339
|
+
@name = name
|
340
|
+
@config = {
|
341
|
+
:terminator => "false",
|
342
|
+
:rescuable => false,
|
343
|
+
:scope => [ :kind ]
|
344
|
+
}.merge(config)
|
345
|
+
end
|
346
|
+
|
347
|
+
def compile(key=nil, object=nil)
|
348
|
+
method = []
|
349
|
+
method << "value = nil"
|
350
|
+
method << "halted = false"
|
351
|
+
|
352
|
+
each do |callback|
|
353
|
+
method << callback.start(key, object)
|
354
|
+
end
|
355
|
+
|
356
|
+
if config[:rescuable]
|
357
|
+
method << "rescued_error = nil"
|
358
|
+
method << "begin"
|
359
|
+
end
|
360
|
+
|
361
|
+
method << "value = yield if block_given? && !halted"
|
362
|
+
|
363
|
+
if config[:rescuable]
|
364
|
+
method << "rescue Exception => e"
|
365
|
+
method << "rescued_error = e"
|
366
|
+
method << "end"
|
367
|
+
end
|
368
|
+
|
369
|
+
reverse_each do |callback|
|
370
|
+
method << callback.end(key, object)
|
371
|
+
end
|
372
|
+
|
373
|
+
method << "raise rescued_error if rescued_error" if config[:rescuable]
|
374
|
+
method << "halted ? false : (block_given? ? value : true)"
|
375
|
+
method.compact.join("\n")
|
376
|
+
end
|
377
|
+
end
|
378
|
+
|
379
|
+
module ClassMethods
|
380
|
+
# Make the run_callbacks :save method. The generated method takes
|
381
|
+
# a block that it'll yield to. It'll call the before and around filters
|
382
|
+
# in order, yield the block, and then run the after filters.
|
383
|
+
#
|
384
|
+
# run_callbacks :save do
|
385
|
+
# save
|
386
|
+
# end
|
387
|
+
#
|
388
|
+
# The run_callbacks :save method can optionally take a key, which
|
389
|
+
# will be used to compile an optimized callback method for each
|
390
|
+
# key. See #define_callbacks for more information.
|
391
|
+
#
|
392
|
+
def __define_runner(symbol) #:nodoc:
|
393
|
+
body = send("_#{symbol}_callbacks").compile(nil)
|
394
|
+
|
395
|
+
silence_warnings do
|
396
|
+
undef_method "_run_#{symbol}_callbacks" if method_defined?("_run_#{symbol}_callbacks")
|
397
|
+
class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
|
398
|
+
def _run_#{symbol}_callbacks(key = nil, &blk)
|
399
|
+
if key
|
400
|
+
name = "_run__\#{self.class.name.hash.abs}__#{symbol}__\#{key.hash.abs}__callbacks"
|
401
|
+
|
402
|
+
unless respond_to?(name)
|
403
|
+
self.class.__create_keyed_callback(name, :#{symbol}, self, &blk)
|
404
|
+
end
|
405
|
+
|
406
|
+
send(name, &blk)
|
407
|
+
else
|
408
|
+
#{body}
|
409
|
+
end
|
410
|
+
end
|
411
|
+
private :_run_#{symbol}_callbacks
|
412
|
+
RUBY_EVAL
|
413
|
+
end
|
414
|
+
end
|
415
|
+
|
416
|
+
# This is called the first time a callback is called with a particular
|
417
|
+
# key. It creates a new callback method for the key, calculating
|
418
|
+
# which callbacks can be omitted because of per_key conditions.
|
419
|
+
#
|
420
|
+
def __create_keyed_callback(name, kind, object, &blk) #:nodoc:
|
421
|
+
@_keyed_callbacks ||= {}
|
422
|
+
@_keyed_callbacks[name] ||= begin
|
423
|
+
str = send("_#{kind}_callbacks").compile(name, object)
|
424
|
+
class_eval "def #{name}() #{str} end", __FILE__, __LINE__
|
425
|
+
true
|
426
|
+
end
|
427
|
+
end
|
428
|
+
|
429
|
+
# This is used internally to append, prepend and skip callbacks to the
|
430
|
+
# CallbackChain.
|
431
|
+
#
|
432
|
+
def __update_callbacks(name, filters = [], block = nil) #:nodoc:
|
433
|
+
type = [:before, :after, :around].include?(filters.first) ? filters.shift : :before
|
434
|
+
options = filters.last.is_a?(Hash) ? filters.pop : {}
|
435
|
+
filters.unshift(block) if block
|
436
|
+
|
437
|
+
([self] + self.descendants).each do |target|
|
438
|
+
chain = target.send("_#{name}_callbacks")
|
439
|
+
yield chain, type, filters, options
|
440
|
+
target.__define_runner(name)
|
441
|
+
end
|
442
|
+
end
|
443
|
+
|
444
|
+
# Set callbacks for a previously defined callback.
|
445
|
+
#
|
446
|
+
# Syntax:
|
447
|
+
# set_callback :save, :before, :before_meth
|
448
|
+
# set_callback :save, :after, :after_meth, :if => :condition
|
449
|
+
# set_callback :save, :around, lambda { |r| stuff; yield; stuff }
|
450
|
+
#
|
451
|
+
# Use skip_callback to skip any defined one.
|
452
|
+
#
|
453
|
+
# When creating or skipping callbacks, you can specify conditions that
|
454
|
+
# are always the same for a given key. For instance, in Action Pack,
|
455
|
+
# we convert :only and :except conditions into per-key conditions.
|
456
|
+
#
|
457
|
+
# before_filter :authenticate, :except => "index"
|
458
|
+
#
|
459
|
+
# becomes
|
460
|
+
#
|
461
|
+
# dispatch_callback :before, :authenticate, :per_key => {:unless => proc {|c| c.action_name == "index"}}
|
462
|
+
#
|
463
|
+
# Per-Key conditions are evaluated only once per use of a given key.
|
464
|
+
# In the case of the above example, you would do:
|
465
|
+
#
|
466
|
+
# run_callbacks(:dispatch, action_name) { ... dispatch stuff ... }
|
467
|
+
#
|
468
|
+
# In that case, each action_name would get its own compiled callback
|
469
|
+
# method that took into consideration the per_key conditions. This
|
470
|
+
# is a speed improvement for ActionPack.
|
471
|
+
#
|
472
|
+
def set_callback(name, *filter_list, &block)
|
473
|
+
mapped = nil
|
474
|
+
|
475
|
+
__update_callbacks(name, filter_list, block) do |chain, type, filters, options|
|
476
|
+
mapped ||= filters.map do |filter|
|
477
|
+
Callback.new(chain, filter, type, options.dup, self)
|
478
|
+
end
|
479
|
+
|
480
|
+
filters.each do |filter|
|
481
|
+
chain.delete_if {|c| c.matches?(type, filter) }
|
482
|
+
end
|
483
|
+
|
484
|
+
options[:prepend] ? chain.unshift(*mapped) : chain.push(*mapped)
|
485
|
+
end
|
486
|
+
end
|
487
|
+
|
488
|
+
# Skip a previously defined callback for a given type.
|
489
|
+
#
|
490
|
+
def skip_callback(name, *filter_list, &block)
|
491
|
+
__update_callbacks(name, filter_list, block) do |chain, type, filters, options|
|
492
|
+
filters.each do |filter|
|
493
|
+
filter = chain.find {|c| c.matches?(type, filter) }
|
494
|
+
|
495
|
+
if filter && options.any?
|
496
|
+
new_filter = filter.clone(chain, self)
|
497
|
+
chain.insert(chain.index(filter), new_filter)
|
498
|
+
new_filter.recompile!(options, options[:per_key] || {})
|
499
|
+
end
|
500
|
+
|
501
|
+
chain.delete(filter)
|
502
|
+
end
|
503
|
+
end
|
504
|
+
end
|
505
|
+
|
506
|
+
# Reset callbacks for a given type.
|
507
|
+
#
|
508
|
+
def reset_callbacks(symbol)
|
509
|
+
callbacks = send("_#{symbol}_callbacks")
|
510
|
+
|
511
|
+
self.descendants.each do |target|
|
512
|
+
chain = target.send("_#{symbol}_callbacks")
|
513
|
+
callbacks.each { |c| chain.delete(c) }
|
514
|
+
target.__define_runner(symbol)
|
515
|
+
end
|
516
|
+
|
517
|
+
callbacks.clear
|
518
|
+
__define_runner(symbol)
|
519
|
+
end
|
520
|
+
|
521
|
+
# Defines callbacks types:
|
522
|
+
#
|
523
|
+
# define_callbacks :validate
|
524
|
+
#
|
525
|
+
# This macro accepts the following options:
|
526
|
+
#
|
527
|
+
# * <tt>:terminator</tt> - Indicates when a before filter is considered
|
528
|
+
# to be halted.
|
529
|
+
#
|
530
|
+
# define_callbacks :validate, :terminator => "result == false"
|
531
|
+
#
|
532
|
+
# In the example above, if any before validate callbacks returns +false+,
|
533
|
+
# other callbacks are not executed. Defaults to "false", meaning no value
|
534
|
+
# halts the chain.
|
535
|
+
#
|
536
|
+
# * <tt>:rescuable</tt> - By default, after filters are not executed if
|
537
|
+
# the given block or a before filter raises an error. Set this option to
|
538
|
+
# true to change this behavior.
|
539
|
+
#
|
540
|
+
# * <tt>:scope</tt> - Indicates which methods should be executed when a class
|
541
|
+
# is given as callback. Defaults to <tt>[:kind]</tt>.
|
542
|
+
#
|
543
|
+
# class Audit
|
544
|
+
# def before(caller)
|
545
|
+
# puts 'Audit: before is called'
|
546
|
+
# end
|
547
|
+
#
|
548
|
+
# def before_save(caller)
|
549
|
+
# puts 'Audit: before_save is called'
|
550
|
+
# end
|
551
|
+
# end
|
552
|
+
#
|
553
|
+
# class Account
|
554
|
+
# include ActiveSupport::Callbacks
|
555
|
+
#
|
556
|
+
# define_callbacks :save
|
557
|
+
# set_callback :save, :before, Audit.new
|
558
|
+
#
|
559
|
+
# def save
|
560
|
+
# run_callbacks :save do
|
561
|
+
# puts 'save in main'
|
562
|
+
# end
|
563
|
+
# end
|
564
|
+
# end
|
565
|
+
#
|
566
|
+
# In the above case whenever you save an account the method <tt>Audit#before</tt> will
|
567
|
+
# be called. On the other hand
|
568
|
+
#
|
569
|
+
# define_callbacks :save, :scope => [:kind, :name]
|
570
|
+
#
|
571
|
+
# would trigger <tt>Audit#before_save</tt> instead. That's constructed by calling
|
572
|
+
# <tt>"#{kind}_#{name}"</tt> on the given instance. In this case "kind" is "before" and
|
573
|
+
# "name" is "save".
|
574
|
+
#
|
575
|
+
# A declaration like
|
576
|
+
#
|
577
|
+
# define_callbacks :save, :scope => [:name]
|
578
|
+
#
|
579
|
+
# would call <tt>Audit#save</tt>.
|
580
|
+
#
|
581
|
+
def define_callbacks(*callbacks)
|
582
|
+
config = callbacks.last.is_a?(Hash) ? callbacks.pop : {}
|
583
|
+
callbacks.each do |callback|
|
584
|
+
extlib_inheritable_reader("_#{callback}_callbacks") do
|
585
|
+
CallbackChain.new(callback, config)
|
586
|
+
end
|
587
|
+
__define_runner(callback)
|
588
|
+
end
|
589
|
+
end
|
590
|
+
end
|
591
|
+
end
|
592
|
+
end
|
593
|
+
end
|