Skip to content

Commit

Permalink
feat: Add support for validating multiple attributes at once
Browse files Browse the repository at this point in the history
This commit adds support for validating multiple attributes at once with
`validate_presence_of`. This is useful when you want to ensure that
multiple attributes are required.

```ruby
class Example
  include ActiveModel::Model

  attr_accessor :attr1, :attr2

  validates_presence_of :attr1, :attr2
end

RSpec.describe Example do
  it do
    expect(subject).to validate_presence_of(:attr1, :attr2)
  end
end
```

We also add support for using qualifiers with multiple attributes.
There's two caveats: if you use a qualifier, it will apply to all
attributes and only the first failure will be reported.

```ruby
class Example
  include ActiveModel::Model

  attr_accessor :attr1, :attr2

  validates_presence_of :attr1, allow_nil: true
  validates_presence_of :attr2, allow_nil: true
end

RSpec.describe Example do
  it do
    expect(subject).to validate_presence_of(:attr1, :attr2)
  end
end

```
  • Loading branch information
matsales28 committed Sep 13, 2024
1 parent 8890f3d commit 8fd0a26
Show file tree
Hide file tree
Showing 7 changed files with 135 additions and 4 deletions.
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
59 changes: 59 additions & 0 deletions lib/shoulda/matchers/active_model/matcher_collection.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
module Shoulda
module Matchers
module ActiveModel
# @private
class MatcherCollection
def initialize(matchers)
@matchers = matchers
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

0 comments on commit 8fd0a26

Please sign in to comment.