require 'ostruct' module Shoulda # :nodoc: module Callback # :nodoc: module Matchers # :nodoc: module ActiveModel # :nodoc: # Ensures that the given model has a callback defined for the given method # # Options: # * before(:lifecycle). Symbol. - define the callback as a callback before the fact. :lifecycle can be :save, :create, :update, :destroy, :validation # * after(:lifecycle). Symbol. - define the callback as a callback after the fact. :lifecycle can be :save, :create, :update, :destroy, :validation, :initialize, :find, :touch # * around(:lifecycle). Symbol. - define the callback as a callback around the fact. :lifecycle can be :save, :create, :update, :destroy # if(:condition). Symbol. - add a positive condition to the callback to be matched against # unless(:condition). Symbol. - add a negative condition to the callback to be matched against # # Examples: # it { should callback(:method).after(:create) } # it { should callback(:method).before(:validation).unless(:should_it_not?) } # it { should callback(CallbackClass).before(:validation).unless(:should_it_not?) } # def callback method CallbackMatcher.new method end class CallbackMatcher # :nodoc: VALID_OPTIONAL_LIFECYCLES = [:validation, :commit, :rollback].freeze include RailsVersionHelper def initialize method @method = method end # @todo replace with %i() as soon as 1.9 is deprecated [:before, :after, :around].each do |hook| define_method hook do |lifecycle| @hook = hook @lifecycle = lifecycle check_for_undefined_callbacks! self end end [:if, :unless].each do |condition_type| define_method condition_type do |condition| @condition_type = condition_type @condition = condition self end end def on optional_lifecycle check_for_valid_optional_lifecycles! @optional_lifecycle = optional_lifecycle self end def matches? subject check_preconditions! callbacks = subject.send :"_#{@lifecycle}_callbacks" callbacks.any? do |callback| has_callback?(subject, callback) && matches_hook?(callback) && matches_conditions?(callback) && matches_optional_lifecycle?(callback) && callback_method_exists?(subject, callback) end end def callback_method_exists? object, callback if is_class_callback?(object, callback) && !callback_object(object, callback).respond_to?(:"#{@hook}_#{@lifecycle}", true) @failure_message = "callback #{@method} is listed as a callback #{@hook} #{@lifecycle}#{optional_lifecycle_phrase}#{condition_phrase}, but the given object does not respond to #{@hook}_#{@lifecycle} (using respond_to?(:#{@hook}_#{@lifecycle}, true)" false elsif !is_class_callback?(object, callback) && !object.respond_to?(callback.filter, true) @failure_message = "callback #{@method} is listed as a callback #{@hook} #{@lifecycle}#{optional_lifecycle_phrase}#{condition_phrase}, but the model does not respond to #{@method} (using respond_to?(:#{@method}, true)" false else true end end def failure_message @failure_message || "expected #{@method} to be listed as a callback #{@hook} #{@lifecycle}#{optional_lifecycle_phrase}#{condition_phrase}, but was not" end def negative_failure_message @failure_message || "expected #{@method} not to be listed as a callback #{@hook} #{@lifecycle}#{optional_lifecycle_phrase}#{condition_phrase}, but was" end def description "callback #{@method} #{@hook} #{@lifecycle}#{optional_lifecycle_phrase}#{condition_phrase}" end private def check_preconditions! check_lifecycle_present! end def check_lifecycle_present! unless @lifecycle raise UsageError, "callback #{@method} can not be tested against an undefined lifecycle, use .before, .after or .around", caller end end def check_for_undefined_callbacks! if [:rollback, :commit].include?(@lifecycle) && @hook != :after raise UsageError, "Can not callback before or around #{@lifecycle}, use after.", caller end end def check_for_valid_optional_lifecycles! unless VALID_OPTIONAL_LIFECYCLES.include?(@lifecycle) raise UsageError, "The .on option is only valid for #{VALID_OPTIONAL_LIFECYCLES.to_sentence} and cannot be used with #{@lifecycle}, use with .before(:validation) or .after(:validation)", caller end end def precondition_failed? @failure_message.present? end def matches_hook? callback callback.kind == @hook end def has_callback? subject, callback has_callback_object?(subject, callback) || has_callback_method?(callback) || has_callback_class?(callback) end def has_callback_method? callback callback.filter == @method end def has_callback_class? callback class_callback_required? && callback.filter.is_a?(@method) end def has_callback_object? subject, callback callback.filter.respond_to?(:match) && callback.filter.match(/\A_callback/) && subject.respond_to?(:"#{callback.filter}_object") && callback_object(subject, callback).class == @method end def matches_conditions? callback if rails_4_1? !@condition || callback.instance_variable_get(:"@#{@condition_type}").include?(@condition) else !@condition || callback.options[@condition_type].include?(@condition) end end def matches_optional_lifecycle? callback if rails_4_1? if_conditions = callback.instance_variable_get(:@if) !@optional_lifecycle || if_conditions.include?(lifecycle_context_string) || active_model_proc_matches_optional_lifecycle?(if_conditions) else !@optional_lifecycle || callback.options[:if].include?(lifecycle_context_string) end end def condition_phrase " #{@condition_type} #{@condition} evaluates to #{@condition_type == :if ? 'true' : 'false'}" if @condition end def optional_lifecycle_phrase " on #{@optional_lifecycle}" if @optional_lifecycle end def lifecycle_context_string if rails_4? rails_4_lifecycle_context_string else rails_3_lifecycle_context_string end end def rails_3_lifecycle_context_string if @lifecycle == :validation "self.validation_context == :#{@optional_lifecycle}" else "transaction_include_action?(:#{@optional_lifecycle})" end end def rails_4_lifecycle_context_string if @lifecycle == :validation "[:#{@optional_lifecycle}].include? self.validation_context" else "transaction_include_any_action?([:#{@optional_lifecycle}])" end end def active_model_proc_matches_optional_lifecycle? if_conditions if_conditions.select{|i| i.is_a? Proc }.any? do |condition| condition.call ValidationContext.new(@optional_lifecycle) end end def class_callback_required? !@method.is_a?(Symbol) && !@method.is_a?(String) end def is_class_callback? subject, callback !callback_object(subject, callback).is_a?(Symbol) && !callback_object(subject, callback).is_a?(String) end def callback_object subject, callback if (rails_3? || rails_4_0?) && !callback.filter.is_a?(Symbol) subject.send("#{callback.filter}_object") else callback.filter end end end ValidationContext = Struct.new :validation_context UsageError = Class.new NameError end end end end