-
-
Notifications
You must be signed in to change notification settings - Fork 909
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
3 changed files
with
268 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
151 changes: 151 additions & 0 deletions
151
lib/shoulda/matchers/active_record/normalize_matcher.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,151 @@ | ||
module Shoulda | ||
module Matchers | ||
module ActiveRecord | ||
# The `normalize` matcher is used to ensure attribute normalizations | ||
# are transforming attribute values as expected. | ||
# | ||
# Take this model for example: | ||
# | ||
# class User < ActiveRecord::Base | ||
# normalizes :email, with: -> email { email.strip.downcase } | ||
# end | ||
# | ||
# You can use `normalize` providing an input and defining the expected | ||
# normalization output: | ||
# | ||
# # RSpec | ||
# RSpec.describe User, type: :model do | ||
# it do | ||
# should normalize(:email).from(" ME@XYZ.COM\n").to("me@xyz.com") | ||
# end | ||
# end | ||
# | ||
# # Minitest (Shoulda) | ||
# class User < ActiveSupport::TestCase | ||
# should normalize(:email).from(" ME@XYZ.COM\n").to("me@xyz.com") | ||
# end | ||
# | ||
# You can use `normalize` to test multiple attributes at once: | ||
# | ||
# class User < ActiveRecord::Base | ||
# normalizes :email, :handle, with: -> value { value.strip.downcase } | ||
# end | ||
# | ||
# # RSpec | ||
# RSpec.describe User, type: :model do | ||
# it do | ||
# should normalize(:email, :handle).from(" Example\n").to("example") | ||
# end | ||
# end | ||
# | ||
# # Minitest (Shoulda) | ||
# class User < ActiveSupport::TestCase | ||
# should normalize(:email, handle).from(" Example\n").to("example") | ||
# end | ||
# | ||
# If the normalization accepts nil values with the `apply_to_nil` option, | ||
# you just need to use `.from(nil).to("Your expected value here")`. | ||
# | ||
# class User < ActiveRecord::Base | ||
# normalizes :name, with: -> name { name&.titleize || 'Untitled' }, | ||
# apply_to_nil: true | ||
# end | ||
# | ||
# # RSpec | ||
# RSpec.describe User, type: :model do | ||
# it { should normalize(:name).from("jane doe").to("Jane Doe") } | ||
# it { should normalize(:name).from(nil).to("Untitled") } | ||
# end | ||
# | ||
# # Minitest (Shoulda) | ||
# class User < ActiveSupport::TestCase | ||
# should normalize(:name).from("jane doe").to("Jane Doe") | ||
# should normalize(:name).from(nil).to("Untitled") | ||
# end | ||
# | ||
# @return [NormalizeMatcher] | ||
# | ||
def normalize(*attributes) | ||
if attributes.empty? | ||
raise ArgumentError, 'need at least one attribute' | ||
else | ||
NormalizeMatcher.new(*attributes) | ||
end | ||
end | ||
|
||
# @private | ||
class NormalizeMatcher | ||
attr_reader :attributes, :from_value, :to_value, :failure_message, | ||
:failure_message_when_negated | ||
|
||
def initialize(*attributes) | ||
@attributes = attributes | ||
end | ||
|
||
def description | ||
%( | ||
normalize #{attributes.to_sentence(last_word_connector: ' and ')} from | ||
‹#{from_value.inspect}› to ‹#{to_value.inspect}› | ||
).squish | ||
end | ||
|
||
def from(value) | ||
@from_value = value | ||
|
||
self | ||
end | ||
|
||
def to(value) | ||
@to_value = value | ||
|
||
self | ||
end | ||
|
||
def matches?(subject) | ||
attributes.all? { |attribute| attribute_matches?(subject, attribute) } | ||
end | ||
|
||
def does_not_match?(subject) | ||
attributes.all? { |attribute| attribute_does_not_match?(subject, attribute) } | ||
end | ||
|
||
private | ||
|
||
def attribute_matches?(subject, attribute) | ||
return true if normalize_attribute?(subject, attribute) | ||
|
||
@failure_message = build_failure_message( | ||
attribute, | ||
subject.class.normalize_value_for(attribute, from_value), | ||
) | ||
false | ||
end | ||
|
||
def attribute_does_not_match?(subject, attribute) | ||
return true unless normalize_attribute?(subject, attribute) | ||
|
||
@failure_message_when_negated = build_failure_message_when_negated(attribute) | ||
false | ||
end | ||
|
||
def normalize_attribute?(subject, attribute) | ||
subject.class.normalize_value_for(attribute, from_value) == to_value | ||
end | ||
|
||
def build_failure_message(attribute, attribute_value) | ||
%( | ||
Expected to normalize #{attribute.inspect} from ‹#{from_value.inspect}› to | ||
‹#{to_value.inspect}› but it was normalized to ‹#{attribute_value.inspect}› | ||
).squish | ||
end | ||
|
||
def build_failure_message_when_negated(attribute) | ||
%( | ||
Expected to not normalize #{attribute.inspect} from ‹#{from_value.inspect}› to | ||
‹#{to_value.inspect}› but it was normalized | ||
).squish | ||
end | ||
end | ||
end | ||
end | ||
end |
116 changes: 116 additions & 0 deletions
116
spec/unit/shoulda/matchers/active_record/normalize_matcher_spec.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
require 'unit_spec_helper' | ||
|
||
describe Shoulda::Matchers::ActiveRecord::NormalizeMatcher, type: :model do | ||
if rails_version >= 7.1 | ||
describe '#description' do | ||
it 'returns the message including the attribute names, from value and to value' do | ||
matcher = normalize(:name, :email).from("jane doe\n").to('Jane Doe') | ||
expect(matcher.description). | ||
to eq('normalize name and email from ‹"jane doe\n"› to ‹"Jane Doe"›') | ||
end | ||
end | ||
|
||
context 'when subject normalizes single attribute correctly' do | ||
it 'matches' do | ||
model = define_model(:User, email: :string) do | ||
normalizes :email, with: -> (email) { email.strip.downcase } | ||
end | ||
|
||
expect(model.new).to normalize(:email).from(" XyZ@EXAMPLE.com\n").to('xyz@example.com') | ||
end | ||
end | ||
|
||
context 'when subject normalizes multiple attributes correctly' do | ||
it 'matches' do | ||
model = define_model(:User, email: :string, name: :string) do | ||
normalizes :email, :name, with: -> (email) { email.strip.downcase } | ||
end | ||
|
||
expect(model.new).to normalize(:email, :name).from(" XyZ\n").to('xyz') | ||
end | ||
end | ||
|
||
context 'when subject normalizes single attribute incorrectly' do | ||
it 'fails' do | ||
model = define_model(:User, email: :string) do | ||
normalizes :email, with: -> (email) { email.titleize } | ||
end | ||
|
||
assertion = lambda do | ||
expect(model.new).to normalize(:email).from(" XyZ@EXAMPLE.com\n").to('xyz@example.com') | ||
end | ||
|
||
message = %( | ||
Expected to normalize :email from ‹" XyZ@EXAMPLE.com\\n"› to ‹"xyz@example.com"› | ||
but it was normalized to ‹"Xy Z@Example.Com\\n"› | ||
).squish | ||
|
||
expect(&assertion).to fail_with_message(message) | ||
end | ||
end | ||
|
||
context 'when subject normalizes just one attribute incorrectly among multiple attributes' do | ||
it 'fails' do | ||
model = define_model(:User, email: :string, name: :string) do | ||
normalizes :name, with: -> (name) { name.titleize.strip } | ||
normalizes :email, with: -> (email) { email.downcase.strip } | ||
end | ||
|
||
assertion = lambda do | ||
expect(model.new).to normalize(:name, :email).from(" JaneDoe\n").to('Jane Doe') | ||
end | ||
|
||
message = %( | ||
Expected to normalize :email from ‹" JaneDoe\\n"› to ‹"Jane Doe"› | ||
but it was normalized to ‹"janedoe"› | ||
).squish | ||
|
||
expect(&assertion).to fail_with_message(message) | ||
end | ||
end | ||
|
||
context 'when subject normalize nil values correctly' do | ||
it 'matches' do | ||
model = define_model(:User, name: :string) do | ||
normalizes :name, with: -> (name) { name&.strip || 'Untitled' }, apply_to_nil: true | ||
end | ||
|
||
record = model.new | ||
|
||
expect(record).to normalize(:name).from(' Jane Doe ').to('Jane Doe') | ||
expect(record).to normalize(:name).from(nil).to('Untitled') | ||
end | ||
end | ||
|
||
context "when subject doesn't normalize attribute that it shouldn't normalize" do | ||
it 'does not match' do | ||
model = define_model(:User, email: :string) | ||
|
||
expect(model.new).not_to normalize(:email). | ||
from(" XyZ@EXAMPLE.com\n"). | ||
to('xyz@example.com') | ||
end | ||
end | ||
|
||
context "when subject normalizes attributes that it shouldn't normalize" do | ||
it 'fails' do | ||
model = define_model(:User, email: :string, name: :string) do | ||
normalizes :email, with: -> (email) { email.strip.downcase } | ||
end | ||
|
||
assertion = lambda do | ||
expect(model.new).not_to normalize(:name, :email). | ||
from(" XyZ@EXAMPLE.com\n"). | ||
to('xyz@example.com') | ||
end | ||
|
||
message = %( | ||
Expected to not normalize :email from ‹" XyZ@EXAMPLE.com\\n"› to ‹"xyz@example.com"› | ||
but it was normalized | ||
).squish | ||
|
||
expect(&assertion).to fail_with_message(message) | ||
end | ||
end | ||
end | ||
end |