Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add support for validating multiple attributes at once #1651

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/shoulda/matchers/active_model.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
require 'shoulda/matchers/active_model/helpers'
require 'shoulda/matchers/active_model/qualifiers'
require 'shoulda/matchers/active_model/matcher_collection'
require 'shoulda/matchers/active_model/validation_matcher'
require 'shoulda/matchers/active_model/validation_matcher/build_description'
require 'shoulda/matchers/active_model/validator'
Expand Down
2 changes: 1 addition & 1 deletion lib/shoulda/matchers/active_model/allow_value_matcher.rb
Original file line number Diff line number Diff line change
Expand Up @@ -458,7 +458,7 @@ def failure_message
message << '.'
else
message << " producing these validation errors:\n\n"
message << validator.all_formatted_validation_error_messages
message << validator.formatted_validation_error_messages
end
end

Expand Down
4 changes: 3 additions & 1 deletion lib/shoulda/matchers/active_model/helpers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ def pretty_error_messages(object)
format_validation_errors(object.errors)
end

def format_validation_errors(errors)
def format_validation_errors(errors, attr = nil)
list_items = errors.to_hash.keys.map do |attribute|
next if attr && attr.to_sym != attribute.to_sym

messages = errors[attribute]
"* #{attribute}: #{messages}"
end
Expand Down
63 changes: 63 additions & 0 deletions lib/shoulda/matchers/active_model/matcher_collection.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
module Shoulda
module Matchers
module ActiveModel
# @private
class MatcherCollection
def initialize(matchers)
@matchers = matchers
end

def description
matchers.map(&:description).join(' and ')
end

def matches?(subject)
@failed_matchers = failed_matchers_for(subject, :matches?)
@failed_matchers.empty?
end

def does_not_match?(subject)
@failed_matchers = failed_matchers_for(subject, :does_not_match?)
@failed_matchers.empty?
end

def failure_message
first_failure_message(:failure_message)
end

def failure_message_when_negated
first_failure_message(:failure_message_when_negated)
end

def method_missing(method, *args, &block)
if all_matchers_respond_to?(method)
matchers.each { |matcher| matcher.send(method, *args, &block) }
self
else
super
end
end

def respond_to_missing?(method, include_private = false)
all_matchers_respond_to?(method) || super
end

private

attr_reader :matchers

def failed_matchers_for(subject, method)
matchers.reject { |matcher| matcher.send(method, subject) }
end

def first_failure_message(method)
@failed_matchers.first&.send(method)
end

def all_matchers_respond_to?(method)
matchers.all? { |matcher| matcher.respond_to?(method) }
end
end
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -147,8 +147,10 @@ module ActiveModel
#
# @return [ValidatePresenceOfMatcher]
#
def validate_presence_of(attr)
ValidatePresenceOfMatcher.new(attr)

def validate_presence_of(*attrs)
matchers = attrs.map { |attr| ValidatePresenceOfMatcher.new(attr) }
MatcherCollection.new(matchers)
end

# @private
Expand Down
4 changes: 4 additions & 0 deletions lib/shoulda/matchers/active_model/validator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ def validation_exception_message
validation_result[:validation_exception_message]
end

def formatted_validation_error_messages
format_validation_errors(all_validation_errors, attribute)
end

protected

attr_reader :attribute, :context, :record
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,69 @@
include UnitTests::ApplicationConfigurationHelpers

context 'a model with a presence validation' do
context 'passing multiple attributes' do
it 'accepts' do
model = define_model 'Example', attr1: :string, attr2: :string do
validates_presence_of(:attr1)
validates_presence_of(:attr2)
end

expect(model.new).to validate_presence_of(:attr1, :attr2)
end

it 'fails when used in the negative' do
model = define_model 'Example', attr1: :string, attr2: :string do
validates_presence_of(:attr1)
end

assertion = lambda do
expect(model.new).not_to validate_presence_of(:attr1, :attr2)
end

message = <<-MESSAGE
Expected Example not to validate that :attr1 cannot be empty/falsy, but
this could not be proved.
After setting :attr1 to ‹nil›, the matcher expected the Example to be
valid, but it was invalid instead, producing these validation errors:

* attr1: ["can't be blank"]
MESSAGE

expect(&assertion).to fail_with_message(message)
end

it 'accepts when using qualifiers' do
model = define_model 'Example', attr1: :string, attr2: :string do
validates_presence_of(:attr1, allow_nil: true)
validates_presence_of(:attr2, allow_nil: true)
end

expect(model.new).to validate_presence_of(:attr1, :attr2).allow_nil
end

it 'rejects when one attribute does not match the qualifier' do
model = define_model 'Example', attr1: :string, attr2: :string do
validates_presence_of(:attr1, allow_nil: true)
validates_presence_of(:attr2)
end

assertion = lambda do
expect(model.new).to validate_presence_of(:attr1, :attr2).allow_nil
end

message = <<-MESSAGE
Expected Example to validate that :attr2 cannot be empty/falsy, but this
could not be proved.
After setting :attr2 to ‹nil›, the matcher expected the Example to be
valid, but it was invalid instead, producing these validation errors:

* attr2: ["can't be blank"]
MESSAGE

expect(&assertion).to fail_with_message(message)
end
end

it 'accepts' do
expect(validating_presence).to matcher
end
Expand Down
Loading