Skip to content

Commit

Permalink
Add with_prefix and with_suffix to define_enum_for
Browse files Browse the repository at this point in the history
In Rails 5, the `enum` macro received two new options: `_prefix` and
`_suffix`. Despite being a weird API, these change the names of the
methods that are generated from the given values. This commit adds
qualifiers to the `define_enum_for` matcher to match.

While we're here, we also update the messaging for the matcher as well,
to reveal the reasons why steps failed (otherwise it gets really
confusing fast).
  • Loading branch information
mcmire committed Jan 26, 2018
1 parent 5d7755e commit c8b3c7a
Show file tree
Hide file tree
Showing 4 changed files with 758 additions and 135 deletions.
225 changes: 185 additions & 40 deletions lib/shoulda/matchers/active_record/define_enum_for_matcher.rb
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,55 @@ module ActiveRecord
# 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]
#
Expand All @@ -94,17 +143,27 @@ def initialize(attribute_name)
@options = {}
end

def with_values(expected_enum_values)
options[:expected_enum_values] = expected_enum_values
def with_values(expected_values)
options[:expected_values] = expected_values
self
end

def with(expected_enum_values)
def with(expected_values)
Shoulda::Matchers.warn_about_deprecated_method(
'The `with` qualifier on `define_enum_for`',
'`with_values`'
'`with_values`',
)
with_values(expected_enum_values)
with_values(expected_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)
Expand All @@ -114,61 +173,129 @@ def backed_by_column_of_type(expected_column_type)

def matches?(subject)
@record = subject
enum_defined? && enum_values_match? && column_type_matches?

enum_defined? &&
enum_values_match? &&
column_type_matches? &&
enum_value_methods_exist?
end

def failure_message
"Expected #{expectation}"
message = "Expected #{model} to #{expectation}"

if failure_reason
message << ". However, #{failure_reason}"
end

message << '.'

Shoulda::Matchers.word_wrap(message)
end
alias :failure_message_for_should :failure_message

def failure_message_when_negated
"Did not expect #{expectation}"
message = "Expected #{model} not to #{expectation}, but it did."
Shoulda::Matchers.word_wrap(message)
end
alias :failure_message_for_should_not :failure_message_when_negated

def description
desc = "define :#{attribute_name} as an enum"
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 '

if options[:expected_enum_values]
desc << " with #{options[:expected_enum_values]}"
description << "#{options[:expected_suffix].inspect}"
end

desc << " and store the value in a column of type #{expected_column_type}"
if expected_enum_entries.any?
description << ', with possible values '
description << Shoulda::Matchers::Util.inspect_value(
expected_enum_entries,
)
end

desc
description
end

protected
private

attr_reader :record, :attribute_name, :options
attr_reader :record, :attribute_name, :options, :failure_reason

def expectation
"#{model.name} to #{description}"
description
end

def expected_enum_values
hashify(options[:expected_enum_values]).with_indifferent_access
def expected_enum_entries
hashify(options[:expected_values])
end

def actual_enum_values
model.send(attribute_name.to_s.pluralize)
def hashify(value)
if value.nil?
return {}
end

if value.is_a?(Array)
new_value = {}

value.each_with_index do |v, i|
new_value[v.to_sym] = i
end

new_value
else
value
end
end

def actual_enum_entries
model.send(attribute_name.to_s.pluralize).symbolize_keys
end

def enum_defined?
model.defined_enums.include?(attribute_name.to_s)
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?
expected_enum_values.empty? || actual_enum_values == expected_enum_values
end
passed =
expected_enum_entries.empty? ||
actual_enum_entries == expected_enum_entries

def expected_column_type
options[:expected_column_type] || :integer
if passed
true
else
@failure_reason =
"the actual enum values for #{attribute_name.inspect} are " +
Shoulda::Matchers::Util.inspect_value(actual_enum_entries)
false
end
end

def column_type_matches?
column.type == expected_column_type.to_sym
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 column
Expand All @@ -179,21 +306,39 @@ def model
record.class
end

def hashify(value)
if value.nil?
return {}
end

if value.is_a?(Array)
new_value = {}
def expected_column_type
options[:expected_column_type] || :integer
end

value.each_with_index do |v, i|
new_value[v] = i
end
def enum_value_methods_exist?
passed = expected_singleton_methods.all? do |method|
model.singleton_methods.include?(method)
end

new_value
if passed
true
else
value
@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_entries.keys.map do |name|
[options[:expected_prefix], name, options[:expected_suffix]].
select(&:present?).
join('_').
to_sym
end
end
end
Expand Down
23 changes: 22 additions & 1 deletion lib/shoulda/matchers/util.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,14 @@ def self.a_or_an(next_word)
end

def self.inspect_value(value)
"‹#{value.inspect}›"
case value
when Hash
inspect_hash(value)
when Range
inspect_range(value)
else
"‹#{value.inspect}›"
end
end

def self.inspect_values(values)
Expand All @@ -52,6 +59,20 @@ def self.inspect_range(range)
"#{inspect_value(range.first)} to #{inspect_value(range.last)}"
end

def self.inspect_hash(hash)
output = '‹{'

output << hash.map { |key, value|
if key.is_a?(Symbol)
"#{key}: #{value.inspect}"
else
"#{key.inspect} => #{value.inspect}"
end
}.join(', ')

output << '}›'
end

def self.dummy_value_for(column_type, array: false)
if array
[dummy_value_for(column_type, array: false)]
Expand Down
4 changes: 4 additions & 0 deletions spec/support/unit/helpers/active_record_versions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ def active_record_supports_enum?
defined?(::ActiveRecord::Enum)
end

def active_record_enum_supports_prefix_and_suffix?
active_record_version >= 5
end

def active_record_supports_has_secure_password?
active_record_version >= 3.1
end
Expand Down
Loading

0 comments on commit c8b3c7a

Please sign in to comment.