thoughtbot--shoulda-matchers/lib/shoulda/matchers/active_record/define_enum_for_matcher.rb

378 lines
11 KiB
Ruby

module Shoulda
module Matchers
module ActiveRecord
# The `define_enum_for` matcher is used to test that the `enum` macro has
# been used to decorate an attribute with enum methods.
#
# class Process < ActiveRecord::Base
# enum status: [:running, :stopped, :suspended]
# end
#
# # RSpec
# RSpec.describe Process, type: :model do
# it { should define_enum_for(:status) }
# end
#
# # Minitest (Shoulda)
# class ProcessTest < ActiveSupport::TestCase
# should define_enum_for(:status)
# end
#
# #### Qualifiers
#
# ##### with_values
#
# Use `with_values` to test that the attribute has been defined with a
# certain set of possible values.
#
# class Process < ActiveRecord::Base
# enum status: [:running, :stopped, :suspended]
# end
#
# # RSpec
# RSpec.describe Process, type: :model do
# it do
# should define_enum_for(:status).
# with_values([:running, :stopped, :suspended])
# end
# end
#
# # Minitest (Shoulda)
# class ProcessTest < ActiveSupport::TestCase
# should define_enum_for(:status).
# with_values([:running, :stopped, :suspended])
# end
#
# ##### backed_by_column_of_type
#
# Use `backed_by_column_of_type` to test that the attribute is of a
# certain column type. (The default is `:integer`.)
#
# class LoanApplication < ActiveRecord::Base
# enum status: {
# active: "active",
# pending: "pending",
# rejected: "rejected"
# }
# end
#
# # RSpec
# RSpec.describe LoanApplication, type: :model do
# it do
# should define_enum_for(:status).
# with_values(
# active: "active",
# pending: "pending",
# rejected: "rejected"
# ).
# backed_by_column_of_type(:string)
# end
# end
#
# # Minitest (Shoulda)
# class LoanApplicationTest < ActiveSupport::TestCase
# should define_enum_for(:status).
# with_values(
# active: "active",
# pending: "pending",
# rejected: "rejected"
# ).
# backed_by_column_of_type(:string)
# end
#
## ##### with_prefix
#
# Use `with_prefix` to test that the enum is defined with a `_prefix`
# option (Rails 5 only). Can take either a boolean or a symbol:
#
# class Issue < ActiveRecord::Base
# enum status: [:open, :closed], _prefix: :old
# end
#
# # RSpec
# RSpec.describe Issue, type: :model do
# it do
# should define_enum_for(:status).
# with_values([:open, :closed]).
# with_prefix(:old)
# end
# end
#
# # Minitest (Shoulda)
# class ProcessTest < ActiveSupport::TestCase
# should define_enum_for(:status).
# with_values([:open, :closed]).
# with_prefix(:old)
# end
#
# ##### with_suffix
#
# Use `with_suffix` to test that the enum is defined with a `_suffix`
# option (Rails 5 only). Can take either a boolean or a symbol:
#
# class Issue < ActiveRecord::Base
# enum status: [:open, :closed], _suffix: true
# end
#
# # RSpec
# RSpec.describe Issue, type: :model do
# it do
# should define_enum_for(:status).
# with_values([:open, :closed]).
# with_suffix
# end
# end
#
# # Minitest (Shoulda)
# class ProcessTest < ActiveSupport::TestCase
# should define_enum_for(:status).
# with_values([:open, :closed]).
# with_suffix
# end
#
# @return [DefineEnumForMatcher]
#
def define_enum_for(attribute_name)
DefineEnumForMatcher.new(attribute_name)
end
# @private
class DefineEnumForMatcher
def initialize(attribute_name)
@attribute_name = attribute_name
@options = { expected_enum_values: [] }
end
def description
description = "define :#{attribute_name} as an enum, backed by "
description << Shoulda::Matchers::Util.a_or_an(expected_column_type)
if options[:expected_prefix]
description << ', using a prefix of '
description << "#{options[:expected_prefix].inspect}"
end
if options[:expected_suffix]
if options[:expected_prefix]
description << ' and'
else
description << ', using'
end
description << ' a suffix of '
description << "#{options[:expected_suffix].inspect}"
end
if presented_expected_enum_values.any?
description << ', with possible values '
description << Shoulda::Matchers::Util.inspect_value(
presented_expected_enum_values,
)
end
description
end
def with_values(expected_enum_values)
options[:expected_enum_values] = expected_enum_values
self
end
def with(expected_enum_values)
Shoulda::Matchers.warn_about_deprecated_method(
'The `with` qualifier on `define_enum_for`',
'`with_values`',
)
with_values(expected_enum_values)
end
def with_prefix(expected_prefix = attribute_name)
options[:expected_prefix] = expected_prefix
self
end
def with_suffix(expected_suffix = attribute_name)
options[:expected_suffix] = expected_suffix
self
end
def backed_by_column_of_type(expected_column_type)
options[:expected_column_type] = expected_column_type
self
end
def matches?(subject)
@record = subject
enum_defined? &&
enum_values_match? &&
column_type_matches? &&
enum_value_methods_exist?
end
def failure_message
message = "Expected #{model} to #{expectation}"
if failure_reason
message << ". However, #{failure_reason}"
end
message << '.'
Shoulda::Matchers.word_wrap(message)
end
def failure_message_when_negated
message = "Expected #{model} not to #{expectation}, but it did."
Shoulda::Matchers.word_wrap(message)
end
private
attr_reader :attribute_name, :options, :record, :failure_reason
def expectation
description
end
def presented_expected_enum_values
if expected_enum_values.is_a?(Hash)
expected_enum_values.symbolize_keys
else
expected_enum_values
end
end
def normalized_expected_enum_values
to_hash(expected_enum_values)
end
def expected_enum_value_names
to_array(expected_enum_values)
end
def expected_enum_values
options[:expected_enum_values]
end
def presented_actual_enum_values
if expected_enum_values.is_a?(Array)
to_array(actual_enum_values)
else
to_hash(actual_enum_values).symbolize_keys
end
end
def normalized_actual_enum_values
to_hash(actual_enum_values)
end
def actual_enum_values
model.send(attribute_name.to_s.pluralize)
end
def enum_defined?
if model.defined_enums.include?(attribute_name.to_s)
true
else
@failure_reason = "no such enum exists in #{model}"
false
end
end
def enum_values_match?
passed =
expected_enum_values.empty? ||
normalized_actual_enum_values == normalized_expected_enum_values
if passed
true
else
@failure_reason =
"the actual enum values for #{attribute_name.inspect} are " +
Shoulda::Matchers::Util.inspect_value(
presented_actual_enum_values,
)
false
end
end
def column_type_matches?
if column.type == expected_column_type.to_sym
true
else
@failure_reason =
"#{attribute_name.inspect} is " +
Shoulda::Matchers::Util.a_or_an(column.type) +
' column'
false
end
end
def expected_column_type
options[:expected_column_type] || :integer
end
def column
model.columns_hash[attribute_name.to_s]
end
def model
record.class
end
def enum_value_methods_exist?
passed = expected_singleton_methods.all? do |method|
model.singleton_methods.include?(method)
end
if passed
true
else
@failure_reason =
if options[:expected_prefix]
if options[:expected_suffix]
'it was defined with either a different prefix, a ' +
'different suffix, or neither one at all'
else
'it was defined with either a different prefix or none at all'
end
elsif options[:expected_suffix]
'it was defined with either a different suffix or none at all'
end
false
end
end
def expected_singleton_methods
expected_enum_value_names.map do |name|
[options[:expected_prefix], name, options[:expected_suffix]].
select(&:present?).
join('_').
to_sym
end
end
def to_hash(value)
if value.is_a?(Array)
value.each_with_index.inject({}) do |hash, (item, index)|
hash.merge(item.to_s => index)
end
else
value.stringify_keys
end
end
def to_array(value)
if value.is_a?(Array)
value.map(&:to_s)
else
value.keys.map(&:to_s)
end
end
end
end
end
end