/remarkable_activerecord/lib/remarkable/active_record/matchers/association_matcher.rb

https://github.com/cavneb/remarkable · Ruby · 283 lines · 129 code · 34 blank · 120 comment · 11 complexity · 29d4012a3ce54664a5448585a8323f95 MD5 · raw file

  1. module Remarkable
  2. module ActiveRecord
  3. module Matchers
  4. class AssociationMatcher < Remarkable::ActiveRecord::Base #:nodoc:
  5. arguments :macro, :collection => :associations, :as => :association
  6. optionals :through, :source, :source_type, :class_name, :foreign_key, :dependent, :join_table, :as
  7. optionals :select, :conditions, :include, :group, :having, :order, :limit, :offset, :finder_sql, :counter_sql
  8. optionals :uniq, :readonly, :validate, :autosave, :polymorphic, :counter_cache, :default => true
  9. collection_assertions :association_exists?, :macro_matches?, :through_exists?, :source_exists?,
  10. :klass_exists?, :join_table_exists?, :foreign_key_exists?, :polymorphic_exists?,
  11. :counter_cache_exists?, :options_match?
  12. protected
  13. def association_exists?
  14. reflection
  15. end
  16. def macro_matches?
  17. reflection.macro == @macro
  18. end
  19. def through_exists?
  20. return true unless @options.key?(:through)
  21. reflection.through_reflection rescue false
  22. end
  23. def source_exists?
  24. return true unless @options.key?(:through)
  25. reflection.source_reflection rescue false
  26. end
  27. # Polymorphic associations does not have a klass.
  28. #
  29. def klass_exists?
  30. return true if @options[:polymorphic]
  31. reflection.klass rescue nil
  32. end
  33. # has_and_belongs_to_many only works if the tables are in the same
  34. # database, so we always look for the table in the subject connection.
  35. #
  36. def join_table_exists?
  37. return true unless reflection.macro == :has_and_belongs_to_many
  38. subject_class.connection.tables.include?(reflection.options[:join_table].to_s)
  39. end
  40. def foreign_key_exists?
  41. return true unless foreign_key_table
  42. table_has_column?(foreign_key_table_class, foreign_key_table, reflection_foreign_key)
  43. end
  44. def polymorphic_exists?
  45. return true unless @options[:polymorphic]
  46. klass_table_has_column?(subject_class, reflection_foreign_key.sub(/_id$/, '_type'))
  47. end
  48. def counter_cache_exists?
  49. return true unless @options[:counter_cache]
  50. klass_table_has_column?(reflection.klass, reflection.counter_cache_column.to_s)
  51. end
  52. def options_match?
  53. actual_options = {}
  54. @options.keys.each do |key|
  55. method = :"reflection_#{key}"
  56. @options[key] = @options[key].to_s
  57. actual_options[key] = (respond_to?(method, true) ? send(method) : reflection.options[key]).to_s
  58. end
  59. return @options == actual_options, :actual => actual_options.inspect
  60. end
  61. private
  62. def reflection
  63. @reflection ||= subject_class.reflect_on_association(@association.to_sym)
  64. end
  65. def subject_table_name
  66. subject_class.table_name.to_s
  67. end
  68. def reflection_class_name
  69. reflection.class_name.to_s rescue nil
  70. end
  71. def reflection_table_name
  72. reflection.klass.table_name.to_s rescue nil
  73. end
  74. def reflection_foreign_key
  75. (reflection.respond_to?(:foreign_key) ? reflection.foreign_key : reflection.primary_key_name).to_s
  76. end
  77. def table_has_column?(klass, table_name, column)
  78. klass.connection.columns(table_name, 'Remarkable column retrieval').any?{|c| c.name == column }
  79. end
  80. def klass_table_has_column?(klass, column)
  81. table_has_column?(klass, klass.table_name, column)
  82. end
  83. # In through we don't check the foreign_key, because it's spread
  84. # accross the through and the source reflection which should be tested
  85. # with their own macros.
  86. #
  87. # In cases a join table exists (has_and_belongs_to_many and through
  88. # associations), we check the foreign key in the join table.
  89. #
  90. # On belongs to, the foreign_key is in the subject class table and in
  91. # other cases it's on the reflection class table.
  92. #
  93. def foreign_key_table
  94. if reflection.options.key?(:through)
  95. nil
  96. elsif reflection.macro == :has_and_belongs_to_many
  97. reflection.options[:join_table]
  98. elsif reflection.macro == :belongs_to
  99. subject_table_name
  100. else
  101. reflection_table_name
  102. end
  103. end
  104. # Returns the foreign key table class to use the proper connection
  105. # when searching for the table and foreign key.
  106. #
  107. def foreign_key_table_class
  108. if [:belongs_to, :has_and_belongs_to_many].include?(reflection.macro)
  109. subject_class
  110. else
  111. reflection.klass
  112. end
  113. end
  114. def interpolation_options
  115. options = {}
  116. options[:macro] = Remarkable.t(@macro, :scope => matcher_i18n_scope, :default => @macro.to_s.gsub("_", ""))
  117. options[:options] = @options.inspect
  118. if @subject && reflection
  119. options.merge!(
  120. :actual_macro => Remarkable.t(reflection.macro, :scope => matcher_i18n_scope, :default => reflection.macro.to_s),
  121. :subject_table => subject_table_name.inspect,
  122. :reflection_table => reflection_table_name.inspect,
  123. :foreign_key_table => foreign_key_table.inspect,
  124. :polymorphic_column => reflection_foreign_key.sub(/_id$/, '_type').inspect,
  125. :counter_cache_column => reflection.counter_cache_column.to_s.inspect,
  126. :join_table => reflection.options[:join_table].inspect,
  127. :foreign_key => reflection_foreign_key.inspect
  128. )
  129. end
  130. options
  131. end
  132. end
  133. # Ensure that the belongs_to relationship exists. Will also test that the
  134. # subject table has the association_id column.
  135. #
  136. # == Options
  137. #
  138. # * <tt>:class_name</tt> - the expected associted class name.
  139. # * <tt>:foreign_key</tt> - the expected foreign key in the subject table.
  140. # * <tt>:dependent</tt> - the expected dependent value for the association.
  141. # * <tt>:readonly</tt> - checks wether readonly is true or false.
  142. # * <tt>:validate</tt> - checks wether validate is true or false.
  143. # * <tt>:autosave</tt> - checks wether autosave is true or false.
  144. # * <tt>:counter_cache</tt> - the expected dependent value for the association.
  145. # It also checks if the column actually exists in the table.
  146. # * <tt>:polymorphic</tt> - if the association should be polymorphic or not.
  147. # When true it also checks for the association_type column in the subject table.
  148. #
  149. # Plus all supported sql conditions options: :select, :conditions, :order,
  150. # :limit, :offset, :include, :group, :having.
  151. #
  152. # == Examples
  153. #
  154. # should_belong_to :parent, :polymorphic => true
  155. # it { should belong_to(:parent) }
  156. #
  157. def belong_to(*associations, &block)
  158. AssociationMatcher.new(:belongs_to, *associations, &block).spec(self)
  159. end
  160. # Ensures that the has_and_belongs_to_many relationship exists, if the join
  161. # table is in place and if the foreign_key column exists.
  162. #
  163. # == Options
  164. #
  165. # * <tt>:class_name</tt> - the expected associted class name.
  166. # * <tt>:join_table</tt> - the expected join table name.
  167. # * <tt>:foreign_key</tt> - the expected foreign key in the association table.
  168. # * <tt>:uniq</tt> - checks wether uniq is true or false.
  169. # * <tt>:readonly</tt> - checks wether readonly is true or false.
  170. # * <tt>:validate</tt> - checks wether validate is true or false.
  171. # * <tt>:autosave</tt> - checks wether autosave is true or false.
  172. #
  173. # Plus all supported sql conditions options: :select, :conditions, :order,
  174. # :limit, :offset, :include, :group, :having.
  175. #
  176. # == Examples
  177. #
  178. # should_have_and_belong_to_many :posts, :cars
  179. # it{ should have_and_belong_to_many :posts, :cars }
  180. #
  181. def have_and_belong_to_many(*associations, &block)
  182. AssociationMatcher.new(:has_and_belongs_to_many, *associations, &block).spec(self)
  183. end
  184. # Ensures that the has_many relationship exists. Will also test that the
  185. # associated table has the required columns. It works by default with
  186. # polymorphic association (:as does not have to be supplied).
  187. #
  188. # == Options
  189. #
  190. # * <tt>:class_name</tt> - the expected associted class name.
  191. # * <tt>:through</tt> - the expected join model which to perform the query.
  192. # It also checks if the through table exists.
  193. # * <tt>:source</tt> - the source of the through association.
  194. # * <tt>:source_type</tt> - the source type of the through association.
  195. # * <tt>:foreign_key</tt> - the expected foreign key in the associated table.
  196. # When used with :through, it will check for the foreign key in the join table.
  197. # * <tt>:dependent</tt> - the expected dependent value for the association.
  198. # * <tt>:uniq</tt> - checks wether uniq is true or false.
  199. # * <tt>:readonly</tt> - checks wether readonly is true or false.
  200. # * <tt>:validate</tt> - checks wether validate is true or false.
  201. # * <tt>:autosave</tt> - checks wether autosave is true or false.
  202. #
  203. # Plus all supported sql conditions options: :select, :conditions, :order,
  204. # :limit, :offset, :include, :group, :having.
  205. #
  206. # == Examples
  207. #
  208. # should_have_many :friends
  209. # should_have_many :enemies, :through => :friends
  210. # should_have_many :enemies, :dependent => :destroy
  211. #
  212. # it{ should have_many(:friends) }
  213. # it{ should have_many(:enemies, :through => :friends) }
  214. # it{ should have_many(:enemies, :dependent => :destroy) }
  215. #
  216. def have_many(*associations, &block)
  217. AssociationMatcher.new(:has_many, *associations, &block).spec(self)
  218. end
  219. # Ensures that the has_many relationship exists. Will also test that the
  220. # associated table has the required columns. It works by default with
  221. # polymorphic association (:as does not have to be supplied).
  222. #
  223. # == Options
  224. #
  225. # * <tt>:class_name</tt> - the expected associted class name.
  226. # * <tt>:through</tt> - the expected join model which to perform the query.
  227. # It also checks if the through table exists.
  228. # * <tt>:source</tt> - the source of the through association.
  229. # * <tt>:source_type</tt> - the source type of the through association.
  230. # * <tt>:foreign_key</tt> - the expected foreign key in the associated table.
  231. # When used with :through, it will check for the foreign key in the join table.
  232. # * <tt>:dependent</tt> - the expected dependent value for the association.
  233. # * <tt>:validate</tt> - checks wether validate is true or false.
  234. # * <tt>:autosave</tt> - checks wether autosave is true or false.
  235. #
  236. # Plus all supported sql conditions options: :select, :conditions, :order,
  237. # :limit, :offset, :include, :group, :having.
  238. #
  239. # == Examples
  240. #
  241. # should_have_one :universe
  242. # it{ should have_one(:universe) }
  243. #
  244. def have_one(*associations, &block)
  245. AssociationMatcher.new(:has_one, *associations, &block).spec(self)
  246. end
  247. end
  248. end
  249. end