Skip to content

Commit

Permalink
Add allow_nil to presence matcher
Browse files Browse the repository at this point in the history
Co-authored-by: cjmao
  • Loading branch information
mcmire committed May 31, 2019
1 parent e169608 commit 834d8d0
Show file tree
Hide file tree
Showing 5 changed files with 170 additions and 18 deletions.
2 changes: 2 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@
existing features that broke with the upgrade. ([#1193])
* Add support for expression indexes (Rails 5, Postgres only) to
`have_db_index`. ([#1211])
* Add `allow_nil` to the `validate_presence_of` matcher. ([#1100])

[#1193]: https://github.com/thoughtbot/shoulda-matchers/pull/1193
[#1211]: https://github.com/thoughtbot/shoulda-matchers/pull/1211
[#1100]: https://github.com/thoughtbot/shoulda-matchers/pull/1100

# 4.0.1

Expand Down
1 change: 1 addition & 0 deletions lib/shoulda/matchers/active_model/qualifiers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ module Qualifiers
end
end

require_relative 'qualifiers/allow_nil'
require_relative 'qualifiers/ignore_interference_by_writer'
require_relative 'qualifiers/ignoring_interference_by_writer'
26 changes: 26 additions & 0 deletions lib/shoulda/matchers/active_model/qualifiers/allow_nil.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
module Shoulda
module Matchers
module ActiveModel
module Qualifiers
# @private
module AllowNil
def initialize(*args)
super
@expects_to_allow_nil = false
end

def allow_nil
@expects_to_allow_nil = true
self
end

protected

def expects_to_allow_nil?
@expects_to_allow_nil
end
end
end
end
end
end
71 changes: 56 additions & 15 deletions lib/shoulda/matchers/active_model/validate_presence_of_matcher.rb
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,27 @@ module ActiveModel
#
# #### Qualifiers
#
# ##### allow_nil
#
# Use `allow_nil` if your model has an optional attribute.
#
# class Robot
# include ActiveModel::Model
# attr_accessor :nickname
#
# validates_presence_of :nickname, allow_nil: true
# end
#
# # RSpec
# RSpec.describe Robot, type: :model do
# it { should validate_presence_of(:nickname).allow_nil }
# end
#
# # Minitest (Shoulda)
# class RobotTest < ActiveSupport::TestCase
# should validate_presence_of(:nickname).allow_nil
# end
#
# ##### on
#
# Use `on` if your validation applies only under a certain context.
Expand Down Expand Up @@ -111,6 +132,8 @@ def validate_presence_of(attr)

# @private
class ValidatePresenceOfMatcher < ValidationMatcher
include Qualifiers::AllowNil

def initialize(attribute)
super
@expected_message = :blank
Expand All @@ -122,9 +145,16 @@ def matches?(subject)
possibly_ignore_interference_by_writer

if secure_password_being_validated?
disallows_and_double_checks_value_of!(blank_value, @expected_message)
ignore_interference_by_writer.default_to(when: :blank?)

disallowed_values.all? do |value|
disallows_and_double_checks_value_of!(value)
end
else
disallows_original_or_typecast_value?(blank_value, @expected_message)
(!expects_to_allow_nil? || allows_value_of(nil)) &&
disallowed_values.all? do |value|
disallows_original_or_typecast_value?(value)
end
end
end

Expand All @@ -134,9 +164,16 @@ def does_not_match?(subject)
possibly_ignore_interference_by_writer

if secure_password_being_validated?
allows_and_double_checks_value_of!(blank_value, @expected_message)
ignore_interference_by_writer.default_to(when: :blank?)

disallowed_values.any? do |value|
allows_and_double_checks_value_of!(value)
end
else
allows_original_or_typecast_value?(blank_value, @expected_message)
(expects_to_allow_nil? && !allows_value_of(nil)) ||
disallowed_values.any? do |value|
allows_original_or_typecast_value?(value)
end
end
end

Expand All @@ -157,31 +194,35 @@ def possibly_ignore_interference_by_writer
end
end

def allows_and_double_checks_value_of!(value, message)
allows_value_of(value, message)
def allows_and_double_checks_value_of!(value)
allows_value_of(value, @expected_message)
rescue ActiveModel::AllowValueMatcher::AttributeChangedValueError
raise ActiveModel::CouldNotSetPasswordError.create(@subject.class)
end

def allows_original_or_typecast_value?(value, message)
allows_value_of(blank_value, @expected_message)
def allows_original_or_typecast_value?(value)
allows_value_of(value, @expected_message)
end

def disallows_and_double_checks_value_of!(value, message)
disallows_value_of(value, message)
def disallows_and_double_checks_value_of!(value)
disallows_value_of(value, @expected_message)
rescue ActiveModel::AllowValueMatcher::AttributeChangedValueError
raise ActiveModel::CouldNotSetPasswordError.create(@subject.class)
end

def disallows_original_or_typecast_value?(value, message)
disallows_value_of(blank_value, @expected_message)
def disallows_original_or_typecast_value?(value)
disallows_value_of(value, @expected_message)
end

def blank_value
def disallowed_values
if collection?
[]
[Array.new]
else
nil
[''].tap do |disallowed|
if !expects_to_allow_nil?
disallowed << nil
end
end
end
end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@
message = <<-MESSAGE
Expected Example to validate that :attr cannot be empty/falsy, but this
could not be proved.
After setting :attr to ‹nil›, the matcher expected the Example to be
After setting :attr to ‹""›, the matcher expected the Example to be
invalid, but it was valid instead.
MESSAGE

Expand Down Expand Up @@ -124,7 +124,7 @@ def model_creator
message = <<-MESSAGE
Expected Example to validate that :attr cannot be empty/falsy, but this
could not be proved.
After setting :attr to ‹nil›, the matcher expected the Example to be
After setting :attr to ‹""›, the matcher expected the Example to be
invalid, but it was valid instead.
MESSAGE

Expand Down Expand Up @@ -355,14 +355,92 @@ def model_creator
validates_presence_of :foo

def foo=(value)
super(Array.wrap(value))
super([])
end
end

expect(model.new).to validate_presence_of(:foo)
end
end

context 'qualified with allow_nil' do
context 'when validating a model with a presence validator' do
context 'and it is specified with allow_nil: true' do
it 'matches in the positive' do
record = validating_presence(allow_nil: true)
expect(record).to matcher.allow_nil
end

it 'does not match in the negative' do
record = validating_presence(allow_nil: true)

assertion = -> { expect(record).not_to matcher.allow_nil }

expect(&assertion).to fail_with_message(<<-MESSAGE)
Expected Example not to validate that :attr cannot be empty/falsy, but
this could not be proved.
After setting :attr to ‹""›, the matcher expected the Example to be
valid, but it was invalid instead, producing these validation errors:
* attr: ["can't be blank"]
MESSAGE
end
end

context 'and it is not specified with allow_nil: true' do
it 'does not match in the positive' do
record = validating_presence

assertion = lambda do
expect(record).to matcher.allow_nil
end

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

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

it 'matches in the negative' do
record = validating_presence

expect(record).not_to matcher.allow_nil
end
end

context 'when validating a model without a presence validator' do
it 'does not match in the positive' do
record = without_validating_presence

assertion = lambda do
expect(record).to matcher.allow_nil
end

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

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

it 'matches in the negative' do
record = without_validating_presence

expect(record).not_to matcher.allow_nil
end
end
end

def matcher
validate_presence_of(:attr)
end
Expand All @@ -373,6 +451,10 @@ def validating_presence(options = {})
end.new
end

def without_validating_presence
define_model(:example, attr: :string).new
end

def active_model(&block)
define_active_model_class('Example', accessors: [:attr], &block).new
end
Expand Down

0 comments on commit 834d8d0

Please sign in to comment.