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:
parent
f50f70e9c8
commit
539c3b349e
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue