From 539c3b349e3fbd7f5760d3569e83b326c5acc56a Mon Sep 17 00:00:00 2001 From: Franck Verrot Date: Fri, 15 May 2015 16:08:41 +0200 Subject: [PATCH] Add `:reject_if` for `accept_nested_attributes_for` This commit implements the `:reject_if` option discussed in #98. `:reject_if` supports literals, `Proc`'s and `Symbol`s: accepts_nested_attributes_for :mirrors, reject_if: proc { |obj| obj.count != 2 } accepts_nested_attributes_for :mirrors, reject_if: :different_than_2? accepts_nested_attributes_for :mirrors, reject_if: false Co-authored-by: Elliot Winkler --- .../accept_nested_attributes_for_matcher.rb | 94 ++++++- ...cept_nested_attributes_for_matcher_spec.rb | 263 +++++++++++++++++- 2 files changed, 351 insertions(+), 6 deletions(-) diff --git a/lib/shoulda/matchers/active_record/accept_nested_attributes_for_matcher.rb b/lib/shoulda/matchers/active_record/accept_nested_attributes_for_matcher.rb index 737b0e52..adc74864 100644 --- a/lib/shoulda/matchers/active_record/accept_nested_attributes_for_matcher.rb +++ b/lib/shoulda/matchers/active_record/accept_nested_attributes_for_matcher.rb @@ -43,6 +43,36 @@ module Shoulda # allow_destroy(true) # end # + # Use `reject_if` to assert that the `:reject_if` option was + # specified, with a Proc (or lambda with an arity of 1) or a + # plain object. + # + # class Car < ActiveRecord::Base + # accepts_nested_attributes_for :mirrors, + # reject_if: proc { |obj| obj.count != 2 } + # + # accepts_nested_attributes_for :mirrors, + # reject_if: :different_than_2? + # + # def different_than_2? + # mirrors.count != 2 + # end + # end + # + # # RSpec + # describe Car do + # it do + # should accept_nested_attributes_for(:mirrors). + # reject_if(:different_than_2?) + # end + # end + # + # # Test::Unit + # class CarTest < ActiveSupport::TestCase + # should accept_nested_attributes_for(:mirrors). + # reject_if(:different_than_2?) + # end + # # ##### limit # # Use `limit` to assert that the `:limit` option was specified. @@ -106,6 +136,11 @@ module Shoulda self end + def reject_if(reject_if) + @options[:reject_if] = reject_if + self + end + def limit(limit) @options[:limit] = limit self @@ -120,6 +155,7 @@ module Shoulda @subject = subject exists? && allow_destroy_correct? && + reject_if_correct? && limit_correct? && update_only_correct? end @@ -137,6 +173,9 @@ module Shoulda if @options.key?(:allow_destroy) description += " allow_destroy => #{@options[:allow_destroy]}" end + if @options.key?(:reject_if) + description += " reject_if => #{@options[:reject_if]}" + end if @options.key?(:limit) description += " limit => #{@options[:limit]}" end @@ -162,6 +201,42 @@ module Shoulda verify_option_is_correct(:allow_destroy, failure_message) end + def reject_if_correct? + if @options.key?(:reject_if) + @problem = nil + problem_prefix = + "reject_if should resolve to #{@options[:reject_if].inspect}" + actual_option_value = config[:reject_if] + + case actual_option_value + when Symbol + if @subject.respond_to?(actual_option_value, true) + resolved_option_value = @subject.send(actual_option_value) + else + @problem = + "#{problem_prefix}, but #{actual_option_value.inspect} " + + "does not exist on #{model_class.name}" + end + when Proc + resolved_option_value = actual_option_value.call(@subject) + else + resolved_option_value = actual_option_value + end + + if @problem + false + elsif @options[:reject_if] == resolved_option_value + true + else + @problem = + "#{problem_prefix}, got #{resolved_option_value.inspect}" + false + end + else + true + end + end + def limit_correct? failure_message = "limit should be #{@options[:limit]}, got #{config[:limit]}" verify_option_is_correct(:limit, failure_message) @@ -172,9 +247,9 @@ module Shoulda verify_option_is_correct(:update_only, failure_message) end - def verify_option_is_correct(option, failure_message) - if @options.key?(option) - if @options[option] == config[option] + def verify_option_is_correct(option_name, failure_message) + if @options.key?(option_name) + if @options[option_name] == resolved_option_for(option_name) true else @problem = failure_message @@ -185,6 +260,19 @@ module Shoulda end end + def resolved_option_for(option_name) + option_value = config[option_name] + + case option_value + when Symbol + @subject.public_send(option_value) + when Proc + option_value.call(@subject) + else + option_value + end + end + def config model_config[@name] end diff --git a/spec/unit/shoulda/matchers/active_record/accept_nested_attributes_for_matcher_spec.rb b/spec/unit/shoulda/matchers/active_record/accept_nested_attributes_for_matcher_spec.rb index 7b3da7da..621fee0e 100644 --- a/spec/unit/shoulda/matchers/active_record/accept_nested_attributes_for_matcher_spec.rb +++ b/spec/unit/shoulda/matchers/active_record/accept_nested_attributes_for_matcher_spec.rb @@ -44,6 +44,256 @@ describe Shoulda::Matchers::ActiveRecord::AcceptNestedAttributesForMatcher, type end end + context 'reject_if' do + context 'when the option on the association is not a proc' do + context 'when the association option is true' do + context 'and the given option is true' do + it 'matches' do + record = accepting_children(reject_if: true) + + expect { children_matcher.reject_if(true) }. + to match_against(record). + or_fail_with(<<-MESSAGE) +Did not expect Parent to accept nested attributes for children + MESSAGE + end + end + + context 'and the given option is false' do + it 'does not match, producing an appropriate message' do + record = accepting_children(reject_if: true) + + expect { children_matcher.reject_if(false) }. + not_to match_against(record). + and_fail_with(<<-MESSAGE) +Expected Parent to accept nested attributes for children (reject_if should resolve to false, got true) + MESSAGE + end + end + end + + context 'when the association option is false' do + context 'and the given option is false' do + it 'matches' do + record = accepting_children(reject_if: false) + + expect { children_matcher.reject_if(false) }. + to match_against(record). + or_fail_with(<<-MESSAGE) +Did not expect Parent to accept nested attributes for children + MESSAGE + end + end + + context 'and the given option is false' do + it 'does not match, producing an appropriate message' do + record = accepting_children(reject_if: false) + + expect { children_matcher.reject_if(true) }. + not_to match_against(record). + and_fail_with(<<-MESSAGE) +Expected Parent to accept nested attributes for children (reject_if should resolve to true, got false) + MESSAGE + end + end + end + end + + context 'when the option on the association is a proc' do + context 'and it returns true' do + context 'and the given option is true' do + it 'matches' do + record = accepting_children(reject_if: ->(_unused) { true }) + + expect { children_matcher.reject_if(true) }. + to match_against(record). + or_fail_with(<<-MESSAGE) +Did not expect Parent to accept nested attributes for children + MESSAGE + end + end + + context 'and the given option is false' do + it 'does not match, producing an appropriate message' do + record = accepting_children(reject_if: ->(_unused) { true }) + + expect { children_matcher.reject_if(false) }. + not_to match_against(record). + and_fail_with(<<-MESSAGE) +Expected Parent to accept nested attributes for children (reject_if should resolve to false, got true) + MESSAGE + end + end + end + + context 'and it returns false' do + context 'and the given option is false' do + it 'matches' do + record = accepting_children(reject_if: ->(_unused) { false }) + + expect { children_matcher.reject_if(false) }. + to match_against(record). + or_fail_with(<<-MESSAGE) +Did not expect Parent to accept nested attributes for children + MESSAGE + end + end + + context 'and the given option is true' do + it 'does not match, producing an appropriate message' do + record = accepting_children(reject_if: ->(_unused) { false }) + + expect { children_matcher.reject_if(true) }. + not_to match_against(record). + and_fail_with(<<-MESSAGE) +Expected Parent to accept nested attributes for children (reject_if should resolve to true, got false) + MESSAGE + end + end + end + end + + context 'when the option on the association is a symbol' do + context 'and a method by that name exists on the model' do + context 'and it is public' do + context 'and it returns true' do + context 'and the given option is true' do + it 'matches' do + record = accepting_children(reject_if: :truthy_method) do + public def truthy_method; true; end + end + + expect { children_matcher.reject_if(true) }. + to match_against(record). + or_fail_with(<<-MESSAGE) +Did not expect Parent to accept nested attributes for children + MESSAGE + end + end + + context 'and the given option is false' do + it 'does not match, producing an appropriate message' do + record = accepting_children(reject_if: :truthy_method) do + public def truthy_method; true; end + end + + expect { children_matcher.reject_if(false) }. + not_to match_against(record). + and_fail_with(<<-MESSAGE) +Expected Parent to accept nested attributes for children (reject_if should resolve to false, got true) + MESSAGE + end + end + end + + context 'and it returns false' do + context 'and the given option is false' do + it 'matches' do + record = accepting_children(reject_if: :falsey_method) do + public def falsey_method; false; end + end + + expect { children_matcher.reject_if(false) }. + to match_against(record). + or_fail_with(<<-MESSAGE) +Did not expect Parent to accept nested attributes for children + MESSAGE + end + end + + context 'and the given option is true' do + it 'does not match, producing an appropriate message' do + record = accepting_children(reject_if: :falsey_method) do + public def falsey_method; false; end + end + + expect { children_matcher.reject_if(true) }. + not_to match_against(record). + and_fail_with(<<-MESSAGE) +Expected Parent to accept nested attributes for children (reject_if should resolve to true, got false) + MESSAGE + end + end + end + end + + context 'and it is private' do + context 'and it returns true' do + context 'and the given option is true' do + it 'matches' do + record = accepting_children(reject_if: :truthy_method) do + private def truthy_method; true; end + end + + expect { children_matcher.reject_if(true) }. + to match_against(record). + or_fail_with(<<-MESSAGE) +Did not expect Parent to accept nested attributes for children + MESSAGE + end + end + + context 'and the given option is false' do + it 'does not match, producing an appropriate message' do + record = accepting_children(reject_if: :truthy_method) do + private def truthy_method; true; end + end + + expect { children_matcher.reject_if(false) }. + not_to match_against(record). + and_fail_with(<<-MESSAGE) + Expected Parent to accept nested attributes for children (reject_if should resolve to false, got true) + MESSAGE + end + end + end + + context 'and it returns false' do + context 'and the given option is false' do + it 'matches' do + record = accepting_children(reject_if: :falsey_method) do + private def falsey_method; false; end + end + + expect { children_matcher.reject_if(false) }. + to match_against(record). + or_fail_with(<<-MESSAGE) + Did not expect Parent to accept nested attributes for children + MESSAGE + end + end + + context 'and the given option is true' do + it 'does not match, producing an appropriate message' do + record = accepting_children(reject_if: :falsey_method) do + private def falsey_method; false; end + end + + expect { children_matcher.reject_if(true) }. + not_to match_against(record). + and_fail_with(<<-MESSAGE) + Expected Parent to accept nested attributes for children (reject_if should resolve to true, got false) + MESSAGE + end + end + end + end + end + + context 'and a method by that name does not exist on the model' do + it 'does not match, producing an appropriate message' do + record = accepting_children(reject_if: :unknown_method) + + expect { children_matcher.reject_if(true) }. + not_to match_against(record). + and_fail_with(<<-MESSAGE) +Expected Parent to accept nested attributes for children (reject_if should resolve to true, but :unknown_method does not exist on Parent) + MESSAGE + end + end + end + end + context 'limit' do it 'accepts a correct value' do expect(accepting_children(limit: 3)).to children_matcher.limit(3) @@ -86,12 +336,19 @@ describe Shoulda::Matchers::ActiveRecord::AcceptNestedAttributesForMatcher, type end end - def accepting_children(options = {}) + def accepting_children(options = {}, &block) define_model :child, parent_id: :integer - define_model :parent do + + parent_model = define_model :parent do has_many :children accepts_nested_attributes_for :children, options - end.new + + if block + class_eval(&block) + end + end + + parent_model.new end def children_matcher