module Remarkable module ActiveRecord module Matchers class AssociationMatcher < Remarkable::ActiveRecord::Base #:nodoc: arguments :macro, :collection => :associations, :as => :association optionals :through, :source, :source_type, :class_name, :foreign_key, :dependent, :join_table, :as optionals :select, :conditions, :include, :group, :having, :order, :limit, :offset, :finder_sql, :counter_sql optionals :uniq, :readonly, :validate, :autosave, :polymorphic, :counter_cache, :default => true collection_assertions :association_exists?, :macro_matches?, :through_exists?, :source_exists?, :klass_exists?, :join_table_exists?, :foreign_key_exists?, :polymorphic_exists?, :counter_cache_exists?, :options_match? protected def association_exists? reflection end def macro_matches? reflection.macro == @macro end def through_exists? return true unless @options.key?(:through) reflection.through_reflection rescue false end def source_exists? return true unless @options.key?(:through) reflection.source_reflection rescue false end # Polymorphic associations does not have a klass. # def klass_exists? return true if @options[:polymorphic] reflection.klass rescue nil end # has_and_belongs_to_many only works if the tables are in the same # database, so we always look for the table in the subject connection. # def join_table_exists? return true unless reflection.macro == :has_and_belongs_to_many subject_class.connection.tables.include?(reflection.options[:join_table].to_s) end def foreign_key_exists? return true unless foreign_key_table table_has_column?(foreign_key_table_class, foreign_key_table, reflection_foreign_key) end def polymorphic_exists? return true unless @options[:polymorphic] klass_table_has_column?(subject_class, reflection_foreign_key.sub(/_id$/, '_type')) end def counter_cache_exists? return true unless @options[:counter_cache] klass_table_has_column?(reflection.klass, reflection.counter_cache_column.to_s) end def options_match? actual_options = {} @options.keys.each do |key| method = :"reflection_#{key}" @options[key] = @options[key].to_s actual_options[key] = (respond_to?(method, true) ? send(method) : reflection.options[key]).to_s end return @options == actual_options, :actual => actual_options.inspect end private def reflection @reflection ||= subject_class.reflect_on_association(@association.to_sym) end def subject_table_name subject_class.table_name.to_s end def reflection_class_name reflection.class_name.to_s rescue nil end def reflection_table_name reflection.klass.table_name.to_s rescue nil end def reflection_foreign_key reflection.primary_key_name.to_s end def table_has_column?(klass, table_name, column) klass.connection.columns(table_name, 'Remarkable column retrieval').any?{|c| c.name == column } end def klass_table_has_column?(klass, column) table_has_column?(klass, klass.table_name, column) end # In through we don't check the foreign_key, because it's spread # accross the through and the source reflection which should be tested # with their own macros. # # In cases a join table exists (has_and_belongs_to_many and through # associations), we check the foreign key in the join table. # # On belongs to, the foreign_key is in the subject class table and in # other cases it's on the reflection class table. # def foreign_key_table if reflection.options.key?(:through) nil elsif reflection.macro == :has_and_belongs_to_many reflection.options[:join_table] elsif reflection.macro == :belongs_to subject_table_name else reflection_table_name end end # Returns the foreign key table class to use the proper connection # when searching for the table and foreign key. # def foreign_key_table_class if [:belongs_to, :has_and_belongs_to_many].include?(reflection.macro) subject_class else reflection.klass end end def interpolation_options options = {} options[:macro] = Remarkable.t(@macro, :scope => matcher_i18n_scope, :default => @macro.to_s.gsub("_", "")) options[:options] = @options.inspect if @subject && reflection options.merge!( :actual_macro => Remarkable.t(reflection.macro, :scope => matcher_i18n_scope, :default => reflection.macro.to_s), :subject_table => subject_table_name.inspect, :reflection_table => reflection_table_name.inspect, :foreign_key_table => foreign_key_table.inspect, :polymorphic_column => reflection_foreign_key.sub(/_id$/, '_type').inspect, :counter_cache_column => reflection.counter_cache_column.to_s.inspect, :join_table => reflection.options[:join_table].inspect, :foreign_key => reflection_foreign_key.inspect ) end options end end # Ensure that the belongs_to relationship exists. Will also test that the # subject table has the association_id column. # # == Options # # * :class_name - the expected associted class name. # * :foreign_key - the expected foreign key in the subject table. # * :dependent - the expected dependent value for the association. # * :readonly - checks wether readonly is true or false. # * :validate - checks wether validate is true or false. # * :autosave - checks wether autosave is true or false. # * :counter_cache - the expected dependent value for the association. # It also checks if the column actually exists in the table. # * :polymorphic - if the association should be polymorphic or not. # When true it also checks for the association_type column in the subject table. # # Plus all supported sql conditions options: :select, :conditions, :order, # :limit, :offset, :include, :group, :having. # # == Examples # # should_belong_to :parent, :polymorphic => true # it { should belong_to(:parent) } # def belong_to(*associations, &block) AssociationMatcher.new(:belongs_to, *associations, &block).spec(self) end # Ensures that the has_and_belongs_to_many relationship exists, if the join # table is in place and if the foreign_key column exists. # # == Options # # * :class_name - the expected associted class name. # * :join_table - the expected join table name. # * :foreign_key - the expected foreign key in the association table. # * :uniq - checks wether uniq is true or false. # * :readonly - checks wether readonly is true or false. # * :validate - checks wether validate is true or false. # * :autosave - checks wether autosave is true or false. # # Plus all supported sql conditions options: :select, :conditions, :order, # :limit, :offset, :include, :group, :having. # # == Examples # # should_have_and_belong_to_many :posts, :cars # it{ should have_and_belong_to_many :posts, :cars } # def have_and_belong_to_many(*associations, &block) AssociationMatcher.new(:has_and_belongs_to_many, *associations, &block).spec(self) end # Ensures that the has_many relationship exists. Will also test that the # associated table has the required columns. It works by default with # polymorphic association (:as does not have to be supplied). # # == Options # # * :class_name - the expected associted class name. # * :through - the expected join model which to perform the query. # It also checks if the through table exists. # * :source - the source of the through association. # * :source_type - the source type of the through association. # * :foreign_key - the expected foreign key in the associated table. # When used with :through, it will check for the foreign key in the join table. # * :dependent - the expected dependent value for the association. # * :uniq - checks wether uniq is true or false. # * :readonly - checks wether readonly is true or false. # * :validate - checks wether validate is true or false. # * :autosave - checks wether autosave is true or false. # # Plus all supported sql conditions options: :select, :conditions, :order, # :limit, :offset, :include, :group, :having. # # == Examples # # should_have_many :friends # should_have_many :enemies, :through => :friends # should_have_many :enemies, :dependent => :destroy # # it{ should have_many(:friends) } # it{ should have_many(:enemies, :through => :friends) } # it{ should have_many(:enemies, :dependent => :destroy) } # def have_many(*associations, &block) AssociationMatcher.new(:has_many, *associations, &block).spec(self) end # Ensures that the has_many relationship exists. Will also test that the # associated table has the required columns. It works by default with # polymorphic association (:as does not have to be supplied). # # == Options # # * :class_name - the expected associted class name. # * :through - the expected join model which to perform the query. # It also checks if the through table exists. # * :source - the source of the through association. # * :source_type - the source type of the through association. # * :foreign_key - the expected foreign key in the associated table. # When used with :through, it will check for the foreign key in the join table. # * :dependent - the expected dependent value for the association. # * :validate - checks wether validate is true or false. # * :autosave - checks wether autosave is true or false. # # Plus all supported sql conditions options: :select, :conditions, :order, # :limit, :offset, :include, :group, :having. # # == Examples # # should_have_one :universe # it{ should have_one(:universe) } # def have_one(*associations, &block) AssociationMatcher.new(:has_one, *associations, &block).spec(self) end end end end