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.
Files changed (40) hide show
  1. data/README.rdoc +98 -0
  2. data/Rakefile.rb +11 -0
  3. data/lib/cassandra_mapper.rb +5 -0
  4. data/lib/cassandra_mapper/base.rb +19 -0
  5. data/lib/cassandra_mapper/connection.rb +9 -0
  6. data/lib/cassandra_mapper/core_ext/array/extract_options.rb +29 -0
  7. data/lib/cassandra_mapper/core_ext/array/wrap.rb +22 -0
  8. data/lib/cassandra_mapper/core_ext/class/inheritable_attributes.rb +232 -0
  9. data/lib/cassandra_mapper/core_ext/kernel/reporting.rb +62 -0
  10. data/lib/cassandra_mapper/core_ext/kernel/singleton_class.rb +13 -0
  11. data/lib/cassandra_mapper/core_ext/module/aliasing.rb +70 -0
  12. data/lib/cassandra_mapper/core_ext/module/attribute_accessors.rb +66 -0
  13. data/lib/cassandra_mapper/core_ext/object/duplicable.rb +65 -0
  14. data/lib/cassandra_mapper/core_ext/string/inflections.rb +160 -0
  15. data/lib/cassandra_mapper/core_ext/string/multibyte.rb +72 -0
  16. data/lib/cassandra_mapper/exceptions.rb +10 -0
  17. data/lib/cassandra_mapper/identity.rb +29 -0
  18. data/lib/cassandra_mapper/indexing.rb +465 -0
  19. data/lib/cassandra_mapper/observable.rb +36 -0
  20. data/lib/cassandra_mapper/persistence.rb +309 -0
  21. data/lib/cassandra_mapper/support/callbacks.rb +136 -0
  22. data/lib/cassandra_mapper/support/concern.rb +31 -0
  23. data/lib/cassandra_mapper/support/dependencies.rb +60 -0
  24. data/lib/cassandra_mapper/support/descendants_tracker.rb +41 -0
  25. data/lib/cassandra_mapper/support/inflections.rb +58 -0
  26. data/lib/cassandra_mapper/support/inflector.rb +7 -0
  27. data/lib/cassandra_mapper/support/inflector/inflections.rb +213 -0
  28. data/lib/cassandra_mapper/support/inflector/methods.rb +143 -0
  29. data/lib/cassandra_mapper/support/inflector/transliterate.rb +99 -0
  30. data/lib/cassandra_mapper/support/multibyte.rb +46 -0
  31. data/lib/cassandra_mapper/support/multibyte/utils.rb +62 -0
  32. data/lib/cassandra_mapper/support/observing.rb +218 -0
  33. data/lib/cassandra_mapper/support/support_callbacks.rb +593 -0
  34. data/test/test_helper.rb +11 -0
  35. data/test/unit/callbacks_test.rb +100 -0
  36. data/test/unit/identity_test.rb +51 -0
  37. data/test/unit/indexing_test.rb +406 -0
  38. data/test/unit/observer_test.rb +56 -0
  39. data/test/unit/persistence_test.rb +561 -0
  40. 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