From 2b68f3859b75846dbc7c0b655e7470ce7a9ebf83 Mon Sep 17 00:00:00 2001 From: Elliot Winkler Date: Mon, 13 May 2013 22:42:12 -0400 Subject: [PATCH] Use relations to define conditions and order on associations In Rails 4, the following construct: has_many :children, conditions: { adopted: true } changes to: has_many :children, lambda { where(adopted: true) } As a result, the way we check the conditions attached to a has_many changes too: instead of accessing `reflection.options`, we have to use `reflection.scope` -- this which refers to the lambda above, so we have to evaluate it and then grab the `where` from the Relation that the lambda returns. --- .../active_record/association_matcher.rb | 4 +- .../association_matchers/model_reflector.rb | 53 +++++++++++++++++ .../association_matchers/option_verifier.rb | 41 ++++++++++--- .../association_matchers/order_matcher.rb | 2 +- lib/shoulda/matchers/rails_shim.rb | 9 ++- .../active_record/association_matcher_spec.rb | 58 +++++++++++++++---- 6 files changed, 142 insertions(+), 25 deletions(-) diff --git a/lib/shoulda/matchers/active_record/association_matcher.rb b/lib/shoulda/matchers/active_record/association_matcher.rb index 9c7d0a79..945b2fab 100644 --- a/lib/shoulda/matchers/active_record/association_matcher.rb +++ b/lib/shoulda/matchers/active_record/association_matcher.rb @@ -251,7 +251,7 @@ module Shoulda # :nodoc: def conditions_correct? if options.key?(:conditions) - if option_verifier.correct_for_string?(:conditions, options[:conditions]) + if option_verifier.correct_for_relation_clause?(:conditions, options[:conditions]) true else @missing = "#{name} should have the following conditions: #{options[:conditions]}" @@ -292,7 +292,7 @@ module Shoulda # :nodoc: def class_has_foreign_key?(klass) if options.key?(:foreign_key) - option_verifier.correct_for?(:foreign_key, options[:foreign_key]) + option_verifier.correct_for_string?(:foreign_key, options[:foreign_key]) else if klass.column_names.include?(foreign_key) true diff --git a/lib/shoulda/matchers/active_record/association_matchers/model_reflector.rb b/lib/shoulda/matchers/active_record/association_matchers/model_reflector.rb index a4dd58dc..b426b4e1 100644 --- a/lib/shoulda/matchers/active_record/association_matchers/model_reflector.rb +++ b/lib/shoulda/matchers/active_record/association_matchers/model_reflector.rb @@ -36,8 +36,61 @@ module Shoulda # :nodoc: end end + def association_relation + if reflection.respond_to?(:scope) && reflection.scope + relation_from_scope(reflection.scope) + else + options = reflection.options + relation = RailsShim.clean_scope(reflection.klass) + if options[:conditions] + relation = relation.where(options[:conditions]) + end + if options[:include] + relation = relation.include(options[:include]) + end + if options[:order] + relation = relation.order(options[:order]) + end + if options[:group] + relation = relation.group(options[:group]) + end + if options[:having] + relation = relation.having(options[:having]) + end + if options[:limit] + relation = relation.limit(options[:limit]) + end + relation + end + end + + def build_relation_with_clause(name, value) + case name + when :conditions then associated_class.where(value) + when :order then associated_class.order(value) + else raise ArgumentError, "Unknown clause '#{name}'" + end + end + + def extract_relation_clause_from(relation, name) + case name + when :conditions then relation.where_values_hash + when :order then relation.order_values.join(', ') + else raise ArgumentError, "Unknown clause '#{name}'" + end + end + private + def relation_from_scope(scope) + # Source: AR::Associations::AssociationScope#eval_scope + if scope.is_a?(::Proc) + associated_class.all.instance_exec(subject, &scope) + else + scope + end + end + attr_reader :subject, :name end end diff --git a/lib/shoulda/matchers/active_record/association_matchers/option_verifier.rb b/lib/shoulda/matchers/active_record/association_matchers/option_verifier.rb index 24ae78de..3cb38496 100644 --- a/lib/shoulda/matchers/active_record/association_matchers/option_verifier.rb +++ b/lib/shoulda/matchers/active_record/association_matchers/option_verifier.rb @@ -7,6 +7,8 @@ module Shoulda # :nodoc: attr_reader :reflector + RELATION_OPTIONS = [:conditions, :order] + def initialize(reflector) @reflector = reflector end @@ -23,12 +25,20 @@ module Shoulda # :nodoc: correct_for?(:hash, name, expected_value) end + def correct_for_relation_clause?(name, expected_value) + correct_for?(:relation_clause, name, expected_value) + end + def actual_value_for(name) - method_name = "actual_value_for_#{name}" - if respond_to?(method_name, true) - __send__(method_name) + if RELATION_OPTIONS.include?(name) + actual_value_for_relation_clause(name) else - reflection.options[name] + method_name = "actual_value_for_#{name}" + if respond_to?(method_name, true) + __send__(method_name) + else + reflection.options[name] + end end end @@ -49,15 +59,28 @@ module Shoulda # :nodoc: def type_cast(type, value) case type - when :string then value.to_s - when :boolean then !!value - when :hash then Hash(value).stringify_keys - else value + when :string, :relation_clause then value.to_s + when :boolean then !!value + when :hash then Hash(value).stringify_keys + else value end end def expected_value_for(name, value) - value + if RELATION_OPTIONS.include?(name) + expected_value_for_relation_clause(name, value) + else + value + end + end + + def expected_value_for_relation_clause(name, value) + relation = reflector.build_relation_with_clause(name, value) + reflector.extract_relation_clause_from(relation, name) + end + + def actual_value_for_relation_clause(name) + reflector.extract_relation_clause_from(reflector.association_relation, name) end def actual_value_for_class_name diff --git a/lib/shoulda/matchers/active_record/association_matchers/order_matcher.rb b/lib/shoulda/matchers/active_record/association_matchers/order_matcher.rb index fde78a49..53246baa 100644 --- a/lib/shoulda/matchers/active_record/association_matchers/order_matcher.rb +++ b/lib/shoulda/matchers/active_record/association_matchers/order_matcher.rb @@ -18,7 +18,7 @@ module Shoulda # :nodoc: def matches?(subject) self.subject = ModelReflector.new(subject, name) - if option_verifier.correct_for_string?(:order, order) + if option_verifier.correct_for_relation_clause?(:order, order) true else self.missing_option = "#{name} should be ordered by #{order}" diff --git a/lib/shoulda/matchers/rails_shim.rb b/lib/shoulda/matchers/rails_shim.rb index ff52d951..ee39007a 100644 --- a/lib/shoulda/matchers/rails_shim.rb +++ b/lib/shoulda/matchers/rails_shim.rb @@ -1,7 +1,6 @@ module Shoulda # :nodoc: module Matchers class RailsShim # :nodoc: - def self.layouts_ivar if rails_major_version >= 4 '@_layouts' @@ -18,6 +17,14 @@ module Shoulda # :nodoc: end end + def self.clean_scope(klass) + if rails_major_version == 4 + klass.all + else + klass.scoped + end + end + def self.rails_major_version Rails::VERSION::MAJOR end diff --git a/spec/shoulda/matchers/active_record/association_matcher_spec.rb b/spec/shoulda/matchers/active_record/association_matcher_spec.rb index 8ee84652..c3f3ad01 100644 --- a/spec/shoulda/matchers/active_record/association_matcher_spec.rb +++ b/spec/shoulda/matchers/active_record/association_matcher_spec.rb @@ -67,8 +67,8 @@ describe Shoulda::Matchers::ActiveRecord::AssociationMatcher do it 'accepts an association with a valid :conditions option' do define_model :parent, :adopter => :boolean - define_model :child, :parent_id => :integer do - belongs_to :parent, :conditions => { :adopter => true } + define_model(:child, :parent_id => :integer).tap do |model| + define_association_with_conditions(model, :belongs_to, :parent, :adopter => true) end Child.new.should belong_to(:parent).conditions(:adopter => true) @@ -297,8 +297,8 @@ describe Shoulda::Matchers::ActiveRecord::AssociationMatcher do it 'accepts an association with a valid :conditions option' do define_model :child, :parent_id => :integer, :adopted => :boolean - define_model :parent do - has_many :children, :conditions => { :adopted => true } + define_model(:parent).tap do |model| + define_association_with_conditions(model, :has_many, :children, :adopted => true) end Parent.new.should have_many(:children).conditions(:adopted => true) @@ -374,8 +374,13 @@ describe Shoulda::Matchers::ActiveRecord::AssociationMatcher do def having_many_children(options = {}) define_model :child, :parent_id => :integer - define_model :parent do - has_many :children, options + define_model(:parent).tap do |model| + if options.key?(:order) + order = options.delete(:order) + define_association_with_order(model, :has_many, :children, order, options) + else + model.has_many :children, options + end end.new end end @@ -449,8 +454,8 @@ describe Shoulda::Matchers::ActiveRecord::AssociationMatcher do it 'accepts an association with a valid :conditions option' do define_model :detail, :person_id => :integer, :disabled => :boolean - define_model :person do - has_one :detail, :conditions => { :disabled => true} + define_model(:person).tap do |model| + define_association_with_conditions(model, :has_one, :detail, :disabled => true) end Person.new.should have_one(:detail).conditions(:disabled => true) @@ -524,8 +529,13 @@ describe Shoulda::Matchers::ActiveRecord::AssociationMatcher do def having_one_detail(options = {}) define_model :detail, :person_id => :integer - define_model :person do - has_one :detail, options + define_model(:person).tap do |model| + if options.key?(:order) + order = options.delete(:order) + define_association_with_order(model, :has_one, :detail, order, options) + else + model.has_one :detail, options + end end.new end end @@ -565,8 +575,8 @@ describe Shoulda::Matchers::ActiveRecord::AssociationMatcher do it 'accepts an association with a valid :conditions option' do define_model :relative, :adopted => :boolean - define_model :person do - has_and_belongs_to_many :relatives, :conditions => { :adopted => true } + define_model(:person).tap do |model| + define_association_with_conditions(model, :has_and_belongs_to_many, :relatives, :adopted => true) end define_model :people_relative, :id => false, :person_id => :integer, :relative_id => :integer @@ -638,4 +648,28 @@ describe Shoulda::Matchers::ActiveRecord::AssociationMatcher do end.new end end + + def define_association_with_conditions(model, macro, name, conditions, other_options={}) + args = [] + options = {} + if Shoulda::Matchers::RailsShim.rails_major_version == 4 + args << lambda { where(conditions) } + else + options[:conditions] = conditions + end + args << options + model.__send__(macro, name, *args) + end + + def define_association_with_order(model, macro, name, order, other_options={}) + args = [] + options = {} + if Shoulda::Matchers::RailsShim.rails_major_version == 4 + args << lambda { order(order) } + else + options[:order] = order + end + args << options + model.__send__(macro, name, *args) + end end