This commit is contained in:
Elliot Winkler 2017-12-26 22:30:32 -06:00
parent 3d007a2b0e
commit ec240c3b52
23 changed files with 1447 additions and 1023 deletions

View File

@ -2,7 +2,6 @@ require 'shoulda/matchers/active_model/helpers'
require 'shoulda/matchers/active_model/qualifiers'
require 'shoulda/matchers/active_model/validation_matcher'
require 'shoulda/matchers/active_model/validation_matcher/build_description'
require 'shoulda/matchers/active_model/validation_matcher/build_expectation'
require 'shoulda/matchers/active_model/validator'
require 'shoulda/matchers/active_model/allow_or_disallow_value_matcher/attribute_changed_value_error'
require 'shoulda/matchers/active_model/allow_or_disallow_value_matcher/attribute_does_not_exist_error'
@ -10,6 +9,7 @@ require 'shoulda/matchers/active_model/allow_or_disallow_value_matcher/attribute
require 'shoulda/matchers/active_model/allow_or_disallow_value_matcher/attribute_setter_and_validator'
require 'shoulda/matchers/active_model/allow_or_disallow_value_matcher/attribute_setters'
require 'shoulda/matchers/active_model/allow_or_disallow_value_matcher/attribute_setters_and_validators'
require 'shoulda/matchers/active_model/allow_or_disallow_value_matcher/build_expectation_description'
require 'shoulda/matchers/active_model/allow_or_disallow_value_matcher/successful_check'
require 'shoulda/matchers/active_model/allow_or_disallow_value_matcher/successful_setting'
require 'shoulda/matchers/active_model/allow_or_disallow_value_matcher'

View File

@ -111,65 +111,6 @@ module Shoulda
ValidationMatcher::BuildDescription.call(self, simple_description)
end
def expectation_description
"Expected #{model} #{expectation}."
end
def positive_aberration_description
validator = result.validator
message = ''
if validator.validation_message_type_matches?
if validator.has_validation_messages?
message << 'The record was invalid, but'
if validator.captured_validation_exception?
message << ' the exception message was '
message << validator.validation_exception_message.inspect
message << ' instead.'
else
message << " it produced these validation errors instead:\n\n"
message << validator.all_formatted_validation_error_messages
end
elsif expects_strict?
message << 'However, no such exception was raised.'
else
message << 'However, no such error was found on '
message << ":#{attribute_to_check_message_against}."
end
elsif validator.captured_validation_exception?
message << 'The record was invalid, but it'
message << ' raised a validation exception '
message << validator.validation_exception_message.inspect
message << ' instead.'
else
message << 'The record was invalid, but '
message << " it produced these validation errors instead:\n\n"
message << validator.all_formatted_validation_error_messages
end
message
end
def negative_aberration_description
validator = result.validator
# description = 'However, '
# if validator.captured_validation_exception?
# description << ' it raised a validation exception with the message '
# description << validator.validation_exception_message.inspect
# description << '.'
# else
# description << " it produced these validation errors:\n\n"
# description << validator.all_formatted_validation_error_messages
# end
# description
"However, it did."
end
def expectation_clauses_for_values_to_preset
attribute_setters_for_values_to_preset.
select(&:attribute_set?).
@ -182,10 +123,6 @@ module Shoulda
map(&:attribute_setter_expectation_clause)
end
def model
subject.class
end
def last_attribute_setter_used
result.attribute_setter
end
@ -194,6 +131,10 @@ module Shoulda
last_attribute_setter_used.value_written
end
def model
subject.class
end
def pretty_print(pp)
Shoulda::Matchers::Util.pretty_print(self, pp, {
was_negated: was_negated?,
@ -342,6 +283,72 @@ module Shoulda
Shoulda::Matchers.word_wrap(message)
end
def positive_aberration_description
validator = result.validator
# description = 'However, '
# if validator.captured_validation_exception?
# description << ' it raised a validation exception with the message '
# description << validator.validation_exception_message.inspect
# description << '.'
# else
# description << " it produced these validation errors:\n\n"
# description << validator.all_formatted_validation_error_messages
# end
# description
'However, it did fail with that error.'
end
def negative_aberration_description
validator = result.validator
message = ''
if validator.validation_message_type_matches?
if validator.has_validation_messages?
message << 'The record did indeed fail validation, but'
if validator.captured_validation_exception?
message << ' the exception message was '
message << validator.validation_exception_message.inspect
message << ' instead.'
else
message << ' it produced these validation errors on '
message << ":#{attribute_to_check_message_against} instead:\n\n"
message << validator.formatted_validation_messages
end
elsif expects_strict?
message << 'However, no such exception was raised.'
else
message << 'However, no such error was found on '
message << ":#{attribute_to_check_message_against}"
if context.present?
message << ' there. (Perhaps the validation was run under a '
message << 'different context?)'
else
message << '.'
end
end
elsif validator.captured_validation_exception?
message << 'The record did indeed fail validation, but it '
message << 'raised a validation exception '
message << validator.validation_exception_message.inspect
message << ' instead.'
elsif validator.has_any_validation_errors?
message << 'The record did indeed fail validation, but instead of '
message << 'raising an exception, it produced errors on '
message << "these attributes:\n\n"
message << validator.all_formatted_validation_errors
else
message << 'However, it did not fail validation.'
end
message
end
private
def run(strategy)

View File

@ -0,0 +1,115 @@
module Shoulda
module Matchers
module ActiveModel
class AllowOrDisallowValueMatcher
# @private
class BuildExpectationDescription
def self.call(matcher, negated:)
new(matcher, negated: negated).call
end
def initialize(matcher, negated:)
@matcher = matcher
@negated = negated
end
def call
parts = [
'With',
attribute_setter_clauses + ',',
'the',
matcher.model,
'was expected',
expectation,
]
parts.join(' ') + '.'
end
private
attr_reader :matcher
def negated?
@negated
end
def attribute_setter_clauses
clauses = [
clauses_for_values_to_preset,
clauses_for_values_to_set,
]
clauses.select(&:present?).join(' and ')
end
def clauses_for_values_to_preset
matcher.expectation_clauses_for_values_to_preset
end
def clauses_for_values_to_set
matcher.expectation_clauses_for_values_to_set
end
def expectation
if matcher.expects_custom_validation_message?
expectation_with_expected_message
else
expectation_without_expected_message
end
end
def expectation_with_expected_message
parts = [
to_or_not_to,
'fail validation',
context_clause,
'by',
error_message_clause,
]
parts.select(&:present?).join(' ')
end
def to_or_not_to
if negated?
'to'
else
'not to'
end
end
def context_clause
if matcher.context.present?
"(context: #{matcher.context.inspect})"
end
end
def error_message_clause
if matcher.expects_strict?
'raising an exception'
else
clause = ''
if matcher.expected_message.is_a?(Regexp)
clause << 'placing an error matching '
else
clause << 'placing the error '
end
clause << "#{matcher.expected_message.inspect} "
clause << "on :#{matcher.attribute_to_check_message_against}"
end
end
def expectation_without_expected_message
if negated?
'be invalid'
else
'be valid'
end
end
end
end
end
end
end

View File

@ -309,42 +309,6 @@ module Shoulda
# "something other than #{inspected_values_to_set}"
# end
def expectation
error_type_clause =
if expects_strict?
'raising an exception'
else
'placing the error'
end
if expected_message
preface =
"not to fail validation by #{error_type_clause} " +
"#{expected_message.inspect} on " +
":#{attribute_to_check_message_against}"
ValidationMatcher::BuildExpectation.call(
self,
preface,
state: :valid,
)
else
ValidationMatcher::BuildExpectation.call(
self,
'to be valid',
state: :valid,
)
end
end
def aberration_description
if was_negated?
positive_aberration_description
else
negative_aberration_description
end
end
def matches?(subject)
super(subject)
@ -366,6 +330,21 @@ module Shoulda
def failure_message_when_negated
negative_failure_message
end
def expectation_description
AllowOrDisallowValueMatcher::BuildExpectationDescription.call(
self,
negated: was_negated?,
)
end
def aberration_description
if was_negated?
negative_aberration_description
else
positive_aberration_description
end
end
end
end
end

View File

@ -7,39 +7,18 @@ module Shoulda
# inspected_values_to_set
# end
def expectation
error_type_clause =
if expects_strict?
'raising an exception'
else
'placing the error'
end
if expected_message
preface =
"to fail validation by #{error_type_clause} " +
"#{expected_message.inspect} on " +
":#{attribute_to_check_message_against}"
ValidationMatcher::BuildExpectation.call(
self,
preface,
state: :invalid,
)
else
ValidationMatcher::BuildExpectation.call(
self,
'to be invalid',
state: :invalid,
)
end
def expectation_description
AllowOrDisallowValueMatcher::BuildExpectationDescription.call(
self,
negated: !was_negated?,
)
end
def aberration_description
if was_negated?
negative_aberration_description
else
positive_aberration_description
else
negative_aberration_description
end
end

View File

@ -16,6 +16,10 @@ module Shoulda
list_items.join("\n")
end
def format_attribute_specific_validation_errors(errors)
errors.map { |error| "* #{error}" }.join("\n")
end
def default_error_message(type, attribute, options = {})
model_name = options.delete(:model_name)
instance = options.delete(:instance)

View File

@ -9,143 +9,92 @@ module Shoulda
:>= => :greater_than_or_equal_to,
:< => :less_than,
:<= => :less_than_or_equal_to,
:== => :equal_to
}
:== => :equal_to,
}.freeze
def initialize(numericality_matcher, value, operator)
super(nil)
unless numericality_matcher.respond_to? :diff_to_compare
raise ArgumentError, 'numericality_matcher is invalid'
def initialize(numericality_matcher, attribute, value, operator)
super(attribute)
if !numericality_matcher.respond_to?(:diff_to_compare)
raise ArgumentError.new(
'The given numericality matcher does not respond to ' +
':diff_to_compare',
)
end
@numericality_matcher = numericality_matcher
@value = value
@operator = operator
@message = ERROR_MESSAGES[operator]
@message = ERROR_MESSAGES.fetch(operator)
# @comparison_expectation = COMPARISON_EXPECTATIONS.fetch(operator)
end
def simple_description
description = ''
# def comparison_description
# "#{comparison_expectation} #{value}"
# end
if expects_strict?
description << ' strictly'
protected
def add_submatchers
comparison_tuples.each do |diff, add_submatcher_method_name|
__send__(add_submatcher_method_name, diff, nil) do |matcher|
matcher.with_message(message, values: { count: value })
end
end
description +
"disallow :#{attribute} from being a number that is not " +
"#{comparison_expectation} #{@value}"
end
def for(attribute)
@attribute = attribute
self
end
def with_message(message)
@expects_custom_validation_message = true
@message = message
self
end
def expects_custom_validation_message?
@expects_custom_validation_message
end
def matches?(subject)
@subject = subject
all_bounds_correct?
end
def failure_message
last_failing_submatcher.failure_message
end
def failure_message_when_negated
last_failing_submatcher.failure_message_when_negated
end
def comparison_description
"#{comparison_expectation} #{@value}"
end
private
def all_bounds_correct?
failing_submatchers.empty?
end
attr_reader :numericality_matcher, :value, :operator, :message,
:comparison_expectation
def failing_submatchers
submatchers_and_results.
select { |x| !x[:matched] }.
map { |x| x[:matcher] }
end
def last_failing_submatcher
failing_submatchers.last
end
def submatchers
@_submatchers ||=
comparison_combos.map do |diff, submatcher_method_name|
matcher = __send__(submatcher_method_name, diff, nil)
matcher.with_message(@message, values: { count: @value })
matcher
end
end
def submatchers_and_results
@_submatchers_and_results ||=
submatchers.map do |matcher|
{ matcher: matcher, matched: matcher.matches?(@subject) }
end
end
def comparison_combos
diffs_to_compare.zip(submatcher_method_names)
end
def submatcher_method_names
assertions.map do |value|
if value
:allow_value_matcher
else
:disallow_value_matcher
end
end
end
def assertions
case @operator
when :>
[false, false, true]
when :>=
[false, true, true]
when :==
[false, true, false]
when :<
[true, false, false]
when :<=
[true, true, false]
end
def comparison_tuples
diffs_to_compare.zip(add_submatcher_method_names)
end
def diffs_to_compare
diff_to_compare = @numericality_matcher.diff_to_compare
values = [-1, 0, 1].map { |sign| @value + (diff_to_compare * sign) }
diff_to_compare = numericality_matcher.diff_to_compare
values = [-1, 0, 1].map { |sign| value + (diff_to_compare * sign) }
if @numericality_matcher.given_numeric_column?
if numericality_matcher.given_numeric_column?
values
else
values.map(&:to_s)
end
end
def comparison_expectation
case @operator
when :> then "greater than"
when :>= then "greater than or equal to"
when :== then "equal to"
when :< then "less than"
when :<= then "less than or equal to"
def add_submatcher_method_names
case operator
when :>
[
:add_submatcher_disallowing,
:add_submatcher_disallowing,
:add_submatcher_allowing,
]
when :>=
[
:add_submatcher_disallowing,
:add_submatcher_allowing,
:add_submatcher_allowing,
]
when :==
[
:add_submatcher_disallowing,
:add_submatcher_allowing,
:add_submatcher_disallowing,
]
when :<
[
:add_submatcher_allowing,
:add_submatcher_disallowing,
:add_submatcher_disallowing,
]
when :<=
[
:add_submatcher_allowing,
:add_submatcher_allowing,
:add_submatcher_disallowing,
]
end
end
end

View File

@ -6,19 +6,10 @@ module Shoulda
class EvenNumberMatcher < NumericTypeMatcher
NON_EVEN_NUMBER_VALUE = 1
def simple_description
description = ''
def initialize(*args)
super
if expects_strict?
description << 'strictly '
end
description +
"disallow :#{attribute} from being an odd number"
end
def allowed_type_adjective
'even'
with_message(:even)
end
def diff_to_compare
@ -27,12 +18,8 @@ module Shoulda
protected
def wrap_disallow_value_matcher(matcher)
matcher.with_message(:even)
end
def disallowed_value
if @numeric_type_matcher.given_numeric_column?
if numericality_matcher.given_numeric_column?
NON_EVEN_NUMBER_VALUE
else
NON_EVEN_NUMBER_VALUE.to_s

View File

@ -5,34 +5,11 @@ module Shoulda
module ActiveModel
module NumericalityMatchers
# @private
class NumericTypeMatcher
extend Forwardable
class NumericTypeMatcher < ValidationMatcher
def initialize(numericality_matcher, attribute)
super(attribute)
def_delegators(
:disallow_value_matcher,
:expects_custom_validation_message?,
:expects_strict?,
:failure_message,
:failure_message_when_negated,
:ignore_interference_by_writer,
:ignoring_interference_by_writer,
:matches?,
:on,
:strict,
:with_message,
)
def initialize(numeric_type_matcher, attribute)
@numeric_type_matcher = numeric_type_matcher
@attribute = attribute
end
def allowed_type_name
'number'
end
def allowed_type_adjective
''
@numericality_matcher = numericality_matcher
end
def diff_to_compare
@ -41,26 +18,15 @@ module Shoulda
protected
attr_reader :attribute
attr_reader :numericality_matcher
def wrap_disallow_value_matcher(matcher)
raise NotImplementedError
def add_submatchers
add_submatcher_disallowing(disallowed_value)
end
def disallowed_value
raise NotImplementedError
end
private
def disallow_value_matcher
@_disallow_value_matcher ||= begin
DisallowValueMatcher.new(disallowed_value).tap do |matcher|
matcher.for(attribute)
wrap_disallow_value_matcher(matcher)
end
end
end
end
end
end

View File

@ -6,19 +6,10 @@ module Shoulda
class OddNumberMatcher < NumericTypeMatcher
NON_ODD_NUMBER_VALUE = 2
def simple_description
description = ''
def initialize(*args)
super
if expects_strict?
description << 'strictly '
end
description +
"disallow :#{attribute} from being an even number"
end
def allowed_type_adjective
'odd'
with_message(:odd)
end
def diff_to_compare
@ -27,12 +18,8 @@ module Shoulda
protected
def wrap_disallow_value_matcher(matcher)
matcher.with_message(:odd)
end
def disallowed_value
if @numeric_type_matcher.given_numeric_column?
if numericality_matcher.given_numeric_column?
NON_ODD_NUMBER_VALUE
else
NON_ODD_NUMBER_VALUE.to_s

View File

@ -6,18 +6,10 @@ module Shoulda
class OnlyIntegerMatcher < NumericTypeMatcher
NON_INTEGER_VALUE = 0.1
def simple_description
description = ''
def initialize(*args)
super
if expects_strict?
description << ' strictly'
end
description + "disallow :#{attribute} from being a decimal number"
end
def allowed_type_name
'integer'
with_message(:not_an_integer)
end
def diff_to_compare
@ -26,12 +18,8 @@ module Shoulda
protected
def wrap_disallow_value_matcher(matcher)
matcher.with_message(:not_an_integer)
end
def disallowed_value
if @numeric_type_matcher.given_numeric_column?
if numericality_matcher.given_numeric_column?
NON_INTEGER_VALUE
else
NON_INTEGER_VALUE.to_s

View File

@ -303,317 +303,228 @@ module Shoulda
end
# @private
class ValidateNumericalityOfMatcher
NUMERIC_NAME = 'number'
NON_NUMERIC_VALUE = 'abcd'
class ValidateNumericalityOfMatcher < ValidationMatcher
NON_NUMERIC_VALUE = 'not-a-number'.freeze
DEFAULT_DIFF_TO_COMPARE = 1
include Qualifiers::IgnoringInterferenceByWriter
attr_reader :diff_to_compare
def initialize(attribute)
super
@attribute = attribute
@submatchers = []
super(attribute)
@options = {
only_integer: false,
allow_nil: false,
comparisons: {},
cardinality: nil,
}
@diff_to_compare = DEFAULT_DIFF_TO_COMPARE
@expects_custom_validation_message = false
@expects_to_allow_nil = false
@expects_strict = false
@allowed_type_adjective = nil
@allowed_type_name = 'number'
@context = nil
@expected_message = nil
end
def strict
@expects_strict = true
self
end
def expects_strict?
@expects_strict
end
def only_integer
prepare_submatcher(
NumericalityMatchers::OnlyIntegerMatcher.new(self, @attribute)
)
options[:only_integer] = true
self
end
def allow_nil
@expects_to_allow_nil = true
prepare_submatcher(
AllowValueMatcher.new(nil)
.for(@attribute)
.with_message(:not_a_number)
)
options[:allow_nil] = true
self
end
def expects_to_allow_nil?
@expects_to_allow_nil
options[:allow_nil]
end
def odd
prepare_submatcher(
NumericalityMatchers::OddNumberMatcher.new(self, @attribute)
)
options[:cardinality] = :odd
self
end
def even
prepare_submatcher(
NumericalityMatchers::EvenNumberMatcher.new(self, @attribute)
)
options[:cardinality] = :even
self
end
def is_greater_than(value)
prepare_submatcher(comparison_matcher_for(value, :>).for(@attribute))
options[:comparisons][:>] = value
self
end
def is_greater_than_or_equal_to(value)
prepare_submatcher(comparison_matcher_for(value, :>=).for(@attribute))
options[:comparisons][:>=] = value
self
end
def is_equal_to(value)
prepare_submatcher(comparison_matcher_for(value, :==).for(@attribute))
options[:comparisons][:==] = value
self
end
def is_less_than(value)
prepare_submatcher(comparison_matcher_for(value, :<).for(@attribute))
options[:comparisons][:<] = value
self
end
def is_less_than_or_equal_to(value)
prepare_submatcher(comparison_matcher_for(value, :<=).for(@attribute))
options[:comparisons][:<=] = value
self
end
def with_message(message)
@expects_custom_validation_message = true
@expected_message = message
self
end
def expects_custom_validation_message?
@expects_custom_validation_message
end
def on(context)
@context = context
self
end
def matches?(subject)
@subject = subject
@number_of_submatchers = @submatchers.size
add_disallow_value_matcher
qualify_submatchers
first_failing_submatcher.nil?
end
def simple_description
description = ''
description << "validate that :#{@attribute} looks like "
description << Shoulda::Matchers::Util.a_or_an(full_allowed_type)
if comparison_descriptions.present?
description << ' ' + comparison_descriptions
end
description
end
def description
ValidationMatcher::BuildDescription.call(self, simple_description)
end
def failure_message
overall_failure_message.dup.tap do |message|
message << "\n"
message << failure_message_for_first_failing_submatcher
end
end
def failure_message_when_negated
overall_failure_message_when_negated.dup.tap do |message|
if submatcher_failure_message_when_negated.present?
raise "hmm, this needs to be implemented."
message << "\n"
message << Shoulda::Matchers.word_wrap(
submatcher_failure_message_when_negated,
indent: 2
)
end
end
end
def given_numeric_column?
attribute_is_active_record_column? &&
[:integer, :float, :decimal].include?(column_type)
end
protected
def simple_description
parts = [
"validate that :#{attribute} looks like",
Shoulda::Matchers::Util.a_or_an(expected_value_description),
]
parts.join(' ')
end
def add_submatchers
add_default_submatcher
add_submatcher_for_only_integer
add_submatcher_for_allow_nil
add_submatcher_for_cardinality
add_submatchers_for_comparisons
end
def add_submatcher(submatcher)
if submatcher.respond_to?(:diff_to_compare)
@diff_to_compare = [diff_to_compare, submatcher.diff_to_compare].max
end
super(submatcher)
end
private
def model
@subject.class
end
def overall_failure_message
Shoulda::Matchers.word_wrap(
"#{model.name} did not properly #{description}."
)
end
def overall_failure_message_when_negated
Shoulda::Matchers.word_wrap(
"Expected #{model.name} not to #{description}, but it did."
)
end
attr_reader :options, :allowed_type_adjective, :allowed_type_name
def attribute_is_active_record_column?
columns_hash.key?(@attribute.to_s)
columns_hash.key?(attribute.to_s)
end
def column_type
columns_hash[@attribute.to_s].type
columns_hash[attribute.to_s].type
end
def columns_hash
if @subject.class.respond_to?(:columns_hash)
@subject.class.columns_hash
if model.respond_to?(:columns_hash)
model.columns_hash
else
{}
end
end
def add_disallow_value_matcher
disallow_value_matcher = DisallowValueMatcher.
new(NON_NUMERIC_VALUE).
for(@attribute).
with_message(:not_a_number)
add_submatcher(disallow_value_matcher)
def add_default_submatcher
add_submatcher_disallowing(
NON_NUMERIC_VALUE,
expected_message || :not_a_number,
)
end
def prepare_submatcher(submatcher)
add_submatcher(submatcher)
submatcher
end
def comparison_matcher_for(value, operator)
NumericalityMatchers::ComparisonMatcher.
new(self, value, operator).
for(@attribute)
end
def add_submatcher(submatcher)
if submatcher.respond_to?(:allowed_type_name)
@allowed_type_name = submatcher.allowed_type_name
end
if submatcher.respond_to?(:allowed_type_adjective)
@allowed_type_adjective = submatcher.allowed_type_adjective
end
if submatcher.respond_to?(:diff_to_compare)
@diff_to_compare = [@diff_to_compare, submatcher.diff_to_compare].max
end
@submatchers << submatcher
end
def qualify_submatchers
@submatchers.each do |submatcher|
if @expects_strict
submatcher.strict(@expects_strict)
end
if @expected_message.present?
submatcher.with_message(@expected_message)
end
if @context
submatcher.on(@context)
end
submatcher.ignoring_interference_by_writer(
ignore_interference_by_writer
def add_submatcher_for_only_integer
if options[:only_integer]
submatcher = build_submatcher(
NumericalityMatchers::OnlyIntegerMatcher,
self,
attribute,
)
add_submatchers_within(submatcher)
end
end
def number_of_submatchers_for_failure_message
if has_been_qualified?
@submatchers.size - 1
def add_submatcher_for_allow_nil
if options[:allow_nil]
add_submatcher_allowing(nil, :not_a_number)
end
end
def add_submatcher_for_cardinality
case options[:cardinality]
when :odd
submatcher = build_submatcher(
NumericalityMatchers::OddNumberMatcher,
self,
attribute,
)
add_submatchers_within(submatcher)
when :even
submatcher = build_submatcher(
NumericalityMatchers::EvenNumberMatcher,
self,
attribute,
)
add_submatchers_within(submatcher)
end
end
def add_submatchers_for_comparisons
options[:comparisons].each do |operator, value|
submatcher = build_submatcher(
NumericalityMatchers::ComparisonMatcher,
self,
attribute,
value,
operator,
)
add_submatchers_within(submatcher)
end
end
def expected_value_description
parts = [
expected_value_cardinality,
expected_value_type,
expected_value_comparisons,
]
parts.select(&:present?).join(' ')
end
def expected_value_cardinality
if options[:cardinality]
options[:cardinality].to_s
end
end
def expected_value_type
if options[:only_integer]
'integer'
else
@submatchers.size
'number'
end
end
def has_been_qualified?
@submatchers.any? do |submatcher|
submatcher.class.parent == NumericalityMatchers
end
end
def expected_value_comparisons
parts = options[:comparisons].map do |operator, value|
subparts = []
def first_failing_submatcher
@_failing_submatchers ||= @submatchers.detect do |submatcher|
!submatcher.matches?(@subject)
end
end
subparts <<
case operator
when :> then 'greater than'
when :>= then 'greater than or equal to'
when :< then 'less than'
when :<= then 'less than or equal to'
when :== then 'equal to'
end
def submatcher_failure_message
first_failing_submatcher.failure_message
end
subparts << value
def submatcher_failure_message_when_negated
first_failing_submatcher.failure_message_when_negated
end
def failure_message_for_first_failing_submatcher
submatcher = first_failing_submatcher
if number_of_submatchers_for_failure_message > 1
submatcher_description = submatcher.simple_description.
sub(/\bvalidate that\b/, 'validates').
sub(/\bdisallow\b/, 'disallows').
sub(/\ballow\b/, 'allows')
submatcher_message =
"In checking that #{model.name} #{submatcher_description}, " +
submatcher.failure_message[0].downcase +
submatcher.failure_message[1..-1]
else
submatcher_message = submatcher.failure_message
subparts.join(' ')
end
Shoulda::Matchers.word_wrap(submatcher_message, indent: 2)
parts.to_sentence
end
def full_allowed_type
"#{@allowed_type_adjective} #{@allowed_type_name}".strip
end
def comparison_descriptions
description_array = submatcher_comparison_descriptions
description_array.empty? ? '' : submatcher_comparison_descriptions.join(' and ')
end
def submatcher_comparison_descriptions
@submatchers.inject([]) do |arr, submatcher|
if submatcher.respond_to? :comparison_description
arr << submatcher.comparison_description
end
arr
def add_submatchers_within(matcher)
matcher.populated_submatchers.each do |submatcher|
add_submatcher(submatcher)
end
end
end

View File

@ -5,16 +5,21 @@ module Shoulda
class ValidationMatcher
include Qualifiers::IgnoringInterferenceByWriter
attr_reader :expected_message, :validation_context
def initialize(attribute)
super
@attribute = attribute
@expects_strict = false
@record = nil
@submatchers = []
@expected_message = nil
@expects_custom_validation_message = false
@_submatchers_added = false
@validation_context = nil
@submatchers = []
@record = nil
@was_negated = nil
@_submatchers_populated = false
end
def description
@ -29,13 +34,13 @@ module Shoulda
end
end
def on(context)
@context = context
def on(validation_context)
@validation_context = validation_context
self
end
def strict
@expects_strict = true
def strict(expects_strict = true)
@expects_strict = expects_strict
self
end
@ -59,10 +64,7 @@ module Shoulda
def matches?(record)
@record = record
if !@_submatchers_added
add_submatchers
@_submatchers_added = true
end
populate_submatchers
all_submatchers_match?.tap do
@was_negated = false
@ -142,13 +144,28 @@ module Shoulda
@was_negated
end
def populated_submatchers
populate_submatchers
submatchers
end
protected
attr_reader :attribute, :context, :record, :submatchers,
:first_failing_submatcher
attr_reader :attribute, :submatchers, :record
def model
record.class
def simple_description
raise NotImplementedError
end
def expectation
description
end
def populate_submatchers
if !@_submatchers_populated
add_submatchers
@_submatchers_populated = true
end
end
def add_submatchers
@ -200,20 +217,47 @@ module Shoulda
)
end
def build_submatcher(matcher_class, *args)
matcher = matcher_class.new(*args).
with_message(expected_message).
on(validation_context).
strict(expects_strict?).
ignoring_interference_by_writer(ignore_interference_by_writer)
yield matcher if block_given?
matcher
end
def model
record.class
end
private
def overall_failure_message
Shoulda::Matchers.word_wrap(<<~MESSAGE)
Your test expecting #{model.name} to #{simple_description} didn't
pass.
MESSAGE
message = "Your test expecting #{model.name} to #{description}"
message <<
if message.include?(',')
", didn't pass."
else
" didn't pass."
end
Shoulda::Matchers.word_wrap(message)
end
def overall_failure_message_when_negated
Shoulda::Matchers.word_wrap(<<~MESSAGE)
Your test expecting #{model.name} not to #{simple_description}
didn't pass.
MESSAGE
message = "Your test expecting #{model.name} not to #{description}"
message <<
if message.include?(',')
", didn't pass."
else
"didn't pass."
end
Shoulda::Matchers.word_wrap(message)
end
def indented_failure_message_for_first_failing_submatcher
@ -262,16 +306,14 @@ module Shoulda
end
def build_allow_or_disallow_value_matcher(matcher_class:, values:, message:)
matcher = matcher_class.new(*values, part_of_larger_matcher: true).
for(attribute).
with_message(message).
on(context).
strict(expects_strict?).
ignoring_interference_by_writer(ignore_interference_by_writer)
yield matcher if block_given?
matcher
build_submatcher(
matcher_class,
*values,
part_of_larger_matcher: true,
) do |matcher|
matcher.for(attribute).with_message(message || expected_message)
yield matcher if block_given?
end
end
class SubmatcherResult

View File

@ -4,35 +4,31 @@ module Shoulda
class ValidationMatcher
# @private
class BuildDescription
def self.call(matcher, main_description, **options)
new(matcher, **options).call(main_description)
def self.call(matcher, main_description)
new(matcher).call(main_description)
end
def initialize(matcher, expectation_state: :agnostic)
def initialize(matcher)
@matcher = matcher
@expectation_state = expectation_state
end
def call(main_description)
if description_clauses_for_qualifiers.present?
[main_description, description_clauses_for_qualifiers].join(', ')
else
main_description
description = decorate_with_validation_context(main_description)
if description_clause_for_allow_blank_or_nil.present?
description << " #{description_clause_for_allow_blank_or_nil}"
end
end
def description_clauses_for_qualifiers
parts = [
description_clause_for_allow_blank_or_nil,
description_clause_for_strict_or_custom_validation_message,
]
if description_clause_for_strict_or_custom_validation_message.present?
description << ", #{description_clause_for_strict_or_custom_validation_message}"
end
parts.select(&:present?).join(', and')
description
end
protected
attr_reader :matcher, :expectation_state
attr_reader :matcher
private
@ -45,50 +41,52 @@ module Shoulda
end
def description_clause_for_strict_or_custom_validation_message
if expectation_state == :agnostic
if expects_strict?
description_clause_for_strict
elsif expects_custom_validation_message?
description_clause_for_custom_validation_message
end
if expects_strict?
description_clause_for_strict
elsif expects_custom_validation_message?
description_clause_for_custom_validation_message
end
end
def description_clause_for_allow_blank
'only if it is not blank'
'(only when not blank)'
end
def description_clause_for_allow_nil
'only if it is not nil'
'(only when not nil)'
end
def description_clause_for_strict
parts = []
'raising a validation exception on failure'
end
parts << 'raising a validation exception'
def description_clause_for_custom_validation_message
parts = ['producing a validation error']
if matcher.try(:expects_custom_validation_message?)
parts << matcher.expected_message.inspect
end
parts <<
if matcher.expected_message.is_a?(Regexp)
"matching #{matcher.expected_message.inspect}"
else
matcher.expected_message.inspect
end
if expectation_state == :agnostic
parts << 'on failure'
end
parts << 'on failure'
parts.join(' ')
end
def description_clause_for_custom_validation_message
parts = [
'producing a validation error',
matcher.expected_message.inspect,
]
if expectation_state == :agnostic
parts << 'on failure'
def decorate_with_validation_context(description)
if validation_context.present?
description.gsub(/\bvalidat(?:e|ion)\b/) do |str|
"#{str} (context: #{validation_context.inspect})"
end
else
description
end
end
parts.join(' ')
def validation_context
matcher.try(:validation_context)
end
def expects_to_allow_blank?

View File

@ -1,54 +0,0 @@
module Shoulda
module Matchers
module ActiveModel
class ValidationMatcher
# @private
class BuildExpectation
def self.call(matcher, preface, **options)
new(matcher, preface, **options).call
end
def initialize(matcher, preface, state:)
@matcher = matcher
@preface = preface
@build_description = BuildDescription.new(
matcher,
expectation_state: state,
)
end
def call
parts = [
[preface, 'with', clauses].join(' '),
build_description.description_clauses_for_qualifiers,
]
parts.select(&:present?).join(', ')
end
private
attr_reader :preface, :matcher, :build_description
def clauses
clauses = [
expectation_clauses_for_values_to_preset,
expectation_clauses_for_values_to_set,
]
clauses.select(&:present?).join(' and ')
end
def expectation_clauses_for_values_to_preset
matcher.expectation_clauses_for_values_to_preset
end
def expectation_clauses_for_values_to_set
matcher.expectation_clauses_for_values_to_set
end
end
end
end
end
end

View File

@ -43,10 +43,18 @@ module Shoulda
validation_messages.any?
end
def all_formatted_validation_error_messages
def has_any_validation_errors?
all_validation_errors.any?
end
def all_formatted_validation_errors
format_validation_errors(all_validation_errors)
end
def formatted_validation_messages
format_attribute_specific_validation_errors(validation_messages)
end
def validation_exception_message
validation_result[:validation_exception_message]
end

View File

@ -37,6 +37,8 @@ module UnitTests
def next_value
if value.is_a?(Array)
value + [value.first.class.new]
elsif value.is_a?(Float)
value + 0.1
elsif value.respond_to?(:next)
value.next
else
@ -100,6 +102,12 @@ module UnitTests
'a'
end
def round_up
Float(value).ceil
rescue ArgumentError
value
end
def change_value(value, value_changer)
self.class.call(column_type, value, value_changer)
end

View File

@ -62,9 +62,9 @@ Your test expecting Example to validate absence of :attr didn't pass.
The matcher ran the following subtests. Those indicated with failed
when they should have passed:
Expected Example to fail validation by placing the error "must be
blank" on :attr with :attr set to "an arbitrary value". However, no
such error was found on :attr.
With :attr set to "an arbitrary value", the Example was expected to
fail validation by placing the error "must be blank" on :attr.
However, no such error was found on :attr.
MESSAGE
assertion = lambda do
@ -106,9 +106,9 @@ Your test expecting Example to validate absence of :attr didn't pass.
The matcher ran the following subtests. Those indicated with failed
when they should have passed:
Expected Example to fail validation by placing the error "must be
blank" on :attr with :attr set to "an arbitrary value". However, no
such error was found on :attr.
With :attr set to "an arbitrary value", the Example was expected to
fail validation by placing the error "must be blank" on :attr.
However, no such error was found on :attr.
MESSAGE
assertion = lambda do
@ -175,8 +175,8 @@ Your test expecting Parent to validate absence of :children didn't pass.
The matcher ran the following subtests. Those indicated with failed
when they should have passed:
Expected Parent to fail validation by placing the error "must be
blank" on :children with :children set to [#<Child id: nil>].
With :children set to [#<Child id: nil>], the Parent was expected to
fail validation by placing the error "must be blank" on :children.
However, no such error was found on :children.
MESSAGE

View File

@ -26,9 +26,9 @@ Your test expecting Example to validate acceptance of :attr didn't pass.
The matcher ran the following subtests. Those indicated with failed
when they should have passed:
Expected Example to fail validation by placing the error "must be
accepted" on :attr with :attr set to false (which was read back as
nil). However, no such error was found on :attr.
With :attr set to false (which was read back as nil), the Example
was expected to fail validation by placing the error "must be
accepted" on :attr. However, no such error was found on :attr.
As indicated above, :attr seems to be changing certain values as they
are set, and this could have something to do with why this matcher is

View File

@ -42,20 +42,21 @@ pass.
The matcher ran the following subtests. Those indicated with failed
when they should have passed:
Expected Example to fail validation by placing the error "doesn't
match Password" on :password_confirmation with :password_confirmation
set to "some value" and :password set to "different value" (which
was read back as "different valuf").
With :password_confirmation set to "some value" and :password set to
"different value" (which was read back as "different valuf"), the
Example was expected to fail validation by placing the error "doesn't
match Password" on :password_confirmation.
Expected Example not to fail validation by placing the error "doesn't
match Password" on :password_confirmation with :password_confirmation
set to "same value" and :password set to "same value" (which was
read back as "same valuf"). However, it did.
With :password_confirmation set to "same value" and :password set to
"same value" (which was read back as "same valuf"), the Example
was expected not to fail validation by placing the error "doesn't
match Password" on :password_confirmation. However, it did fail with
that error.
Expected Example not to fail validation by placing the error "doesn't
match Password" on :password_confirmation with :password_confirmation
set to nil and :password set to "any value" (which was read back
as "any valuf").
With :password_confirmation set to nil and :password set to "any
value" (which was read back as "any valuf"), the Example was
expected not to fail validation by placing the error "doesn't match
Password" on :password_confirmation.
As indicated above, :password seems to be changing certain values as
they are set, and this could have something to do with why this matcher
@ -126,21 +127,21 @@ Your test expecting Example to validate confirmation of
The matcher ran the following subtests. Those indicated with failed
when they should have passed:
Expected Example to fail validation by placing the error "doesn't
match Attribute to confirm" on :attribute_to_confirm_confirmation with
:attribute_to_confirm_confirmation set to "some value" and
:attribute_to_confirm set to "different value". However, no such
error was found on :attribute_to_confirm_confirmation.
With :attribute_to_confirm_confirmation set to "some value" and
:attribute_to_confirm set to "different value", the Example was
expected to fail validation by placing the error "doesn't match
Attribute to confirm" on :attribute_to_confirm_confirmation. However,
no such error was found on :attribute_to_confirm_confirmation.
Expected Example not to fail validation by placing the error "doesn't
match Attribute to confirm" on :attribute_to_confirm_confirmation with
:attribute_to_confirm_confirmation set to "same value" and
:attribute_to_confirm set to "same value".
With :attribute_to_confirm_confirmation set to "same value" and
:attribute_to_confirm set to "same value", the Example was expected
not to fail validation by placing the error "doesn't match Attribute
to confirm" on :attribute_to_confirm_confirmation.
Expected Example not to fail validation by placing the error "doesn't
match Attribute to confirm" on :attribute_to_confirm_confirmation with
:attribute_to_confirm_confirmation set to nil and
:attribute_to_confirm set to "any value".
With :attribute_to_confirm_confirmation set to nil and
:attribute_to_confirm set to "any value", the Example was expected
not to fail validation by placing the error "doesn't match Attribute
to confirm" on :attribute_to_confirm_confirmation.
MESSAGE
expect(&assertion).to fail_with_message(message)

View File

@ -31,19 +31,21 @@ range 2 to 5 didn't pass.
The matcher ran the following subtests. Those indicated with failed
when they should have passed:
Expected Example not to fail validation by placing the error "is
reserved" on :attr with :attr set to 1 (which was read back as 2).
However, it did.
With :attr set to 1 (which was read back as 2), the Example was
expected not to fail validation by placing the error "is reserved" on
:attr. However, it did fail with that error.
Expected Example to fail validation by placing the error "is reserved"
on :attr with :attr set to 2 (which was read back as 3).
With :attr set to 2 (which was read back as 3), the Example was
expected to fail validation by placing the error "is reserved" on
:attr.
Expected Example not to fail validation by placing the error "is
reserved" on :attr with :attr set to 6 (which was read back as 7).
With :attr set to 6 (which was read back as 7), the Example was
expected not to fail validation by placing the error "is reserved" on
:attr.
Expected Example to fail validation by placing the error "is reserved"
on :attr with :attr set to 5 (which was read back as 6). However,
no such error was found on :attr.
With :attr set to 5 (which was read back as 6), the Example was
expected to fail validation by placing the error "is reserved" on
:attr. However, no such error was found on :attr.
As indicated above, :attr seems to be changing certain values as they
are set, and this could have something to do with why this matcher is
@ -179,13 +181,13 @@ nor "two" didn't pass.
The matcher ran the following subtests. Those indicated with failed
when they should have passed:
Expected Example to fail validation by placing the error "is reserved"
on :attr with :attr set to "one" (which was read back as "onf").
However, no such error was found on :attr.
With :attr set to "one" (which was read back as "onf"), the
Example was expected to fail validation by placing the error "is
reserved" on :attr. However, no such error was found on :attr.
Expected Example to fail validation by placing the error "is reserved"
on :attr with :attr set to "two" (which was read back as "twp").
However, no such error was found on :attr.
With :attr set to "two" (which was read back as "twp"), the
Example was expected to fail validation by placing the error "is
reserved" on :attr. However, no such error was found on :attr.
As indicated above, :attr seems to be changing certain values as they
are set, and this could have something to do with why this matcher is

View File

@ -39,13 +39,14 @@ least 4 didn't pass.
The matcher ran the following subtests. Those indicated with failed
when they should have passed:
Expected Example to fail validation by placing the error "is too short
(minimum is 4 characters)" on :attr with :attr set to "xxx" (which
was read back as "xxxa"). However, no such error was found on :attr.
With :attr set to "xxx" (which was read back as "xxxa"), the
Example was expected to fail validation by placing the error "is too
short (minimum is 4 characters)" on :attr. However, no such error was
found on :attr.
Expected Example not to fail validation by placing the error "is too
short (minimum is 4 characters)" on :attr with :attr set to "xxxx"
(which was read back as "xxxxa").
With :attr set to "xxxx" (which was read back as "xxxxa"), the
Example was expected not to fail validation by placing the error "is
too short (minimum is 4 characters)" on :attr.
As indicated above, :attr seems to be changing certain values as they
are set, and this could have something to do with why this matcher is
@ -111,13 +112,14 @@ most 4 didn't pass.
The matcher ran the following subtests. Those indicated with failed
when they should have passed:
Expected Example to fail validation by placing the error "is too long
(maximum is 4 characters)" on :attr with :attr set to "xxxxx" (which
was read back as "xxxx"). However, no such error was found on :attr.
With :attr set to "xxxxx" (which was read back as "xxxx"), the
Example was expected to fail validation by placing the error "is too
long (maximum is 4 characters)" on :attr. However, no such error was
found on :attr.
Expected Example not to fail validation by placing the error "is too
long (maximum is 4 characters)" on :attr with :attr set to "xxxx"
(which was read back as "xxx").
With :attr set to "xxxx" (which was read back as "xxx"), the
Example was expected not to fail validation by placing the error "is
too long (maximum is 4 characters)" on :attr.
As indicated above, :attr seems to be changing certain values as they
are set, and this could have something to do with why this matcher is
@ -176,22 +178,24 @@ didn't pass.
The matcher ran the following subtests. Those indicated with failed
when they should have passed:
Expected Example to fail validation by placing the error "is the wrong
length (should be 4 characters)" on :attr with :attr set to "xxx"
(which was read back as "xxxa"). However, no such error was found on
:attr.
With :attr set to "xxx" (which was read back as "xxxa"), the
Example was expected to fail validation by placing the error "is the
wrong length (should be 4 characters)" on :attr. However, no such
error was found on :attr.
Expected Example not to fail validation by placing the error "is the
wrong length (should be 4 characters)" on :attr with :attr set to
"xxxx" (which was read back as "xxxxa"). However, it did.
With :attr set to "xxxx" (which was read back as "xxxxa"), the
Example was expected not to fail validation by placing the error "is
the wrong length (should be 4 characters)" on :attr. However, it did
fail with that error.
Expected Example to fail validation by placing the error "is the wrong
length (should be 4 characters)" on :attr with :attr set to "xxxxx"
(which was read back as "xxxxxa").
With :attr set to "xxxxx" (which was read back as "xxxxxa"), the
Example was expected to fail validation by placing the error "is the
wrong length (should be 4 characters)" on :attr.
Expected Example not to fail validation by placing the error "is the
wrong length (should be 4 characters)" on :attr with :attr set to
"xxxx" (which was read back as "xxxxa"). However, it did.
With :attr set to "xxxx" (which was read back as "xxxxa"), the
Example was expected not to fail validation by placing the error "is
the wrong length (should be 4 characters)" on :attr. However, it did
fail with that error.
As indicated above, :attr seems to be changing certain values as they
are set, and this could have something to do with why this matcher is