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 <elliot.winkler@gmail.com>
This commit is contained in:
Franck Verrot 2015-05-15 16:08:41 +02:00 committed by Elliot Winkler
parent f50f70e9c8
commit 539c3b349e
2 changed files with 351 additions and 6 deletions

View File

@ -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

View File

@ -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