Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add prefix and suffix to define enum for #1077

Merged
merged 5 commits into from
Jan 28, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
267 changes: 225 additions & 42 deletions lib/shoulda/matchers/active_record/define_enum_for_matcher.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ module ActiveRecord
#
# #### Qualifiers
#
# ##### with
# ##### with_values
#
# Use `with` to test that the attribute has been defined with a certain
# set of known 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]
Expand All @@ -33,14 +33,14 @@ module ActiveRecord
# RSpec.describe Process, type: :model do
# it do
# should define_enum_for(:status).
# with([:running, :stopped, :suspended])
# with_values([:running, :stopped, :suspended])
# end
# end
#
# # Minitest (Shoulda)
# class ProcessTest < ActiveSupport::TestCase
# should define_enum_for(:status).
# with([:running, :stopped, :suspended])
# with_values([:running, :stopped, :suspended])
# end
#
# ##### backed_by_column_of_type
Expand All @@ -60,7 +60,7 @@ module ActiveRecord
# RSpec.describe LoanApplication, type: :model do
# it do
# should define_enum_for(:status).
# with(
# with_values(
# active: "active",
# pending: "pending",
# rejected: "rejected"
Expand All @@ -72,14 +72,63 @@ module ActiveRecord
# # Minitest (Shoulda)
# class LoanApplicationTest < ActiveSupport::TestCase
# should define_enum_for(:status).
# with(
# 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]
#
Expand All @@ -91,76 +140,178 @@ def define_enum_for(attribute_name)
class DefineEnumForMatcher
def initialize(attribute_name)
@attribute_name = attribute_name
@options = {}
@options = { expected_enum_values: [] }
end

def with(expected_enum_values)
def description

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assignment Branch Condition size for description is too high. [28.3/15]
Method has too many lines. [22/10]

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}"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefer to_s over string interpolation.

end

if options[:expected_suffix]
if options[:expected_prefix]

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use the return of the conditional for variable assignment and comparison.

description << ' and'
else
description << ', using'
end

description << ' a suffix of '

description << "#{options[:expected_suffix].inspect}"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefer to_s over string interpolation.

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_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"

if options[:expected_enum_values]
desc << " with #{options[:expected_enum_values]}"
end
private

desc << " and store the value in a column of type #{expected_column_type}"
attr_reader :attribute_name, :options, :record, :failure_reason

desc
def expectation
description
end

protected
def presented_expected_enum_values
if expected_enum_values.is_a?(Hash)
expected_enum_values.symbolize_keys
else
expected_enum_values
end
end

attr_reader :record, :attribute_name, :options
def normalized_expected_enum_values
to_hash(expected_enum_values)
end

def expectation
"#{model.name} to #{description}"
def expected_enum_value_names
to_array(expected_enum_values)
end

def expected_enum_values
hashify(options[:expected_enum_values]).with_indifferent_access
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?
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_values.empty? ||
normalized_actual_enum_values == normalized_expected_enum_values

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(
presented_actual_enum_values,
)
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 expected_column_type
options[:expected_column_type] || :integer
end

def column
Expand All @@ -171,21 +322,53 @@ def model
record.class
end

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Method has too many lines. [19/10]
Perceived complexity for enum_value_methods_exist? is too high. [8/7]

passed = expected_singleton_methods.all? do |method|
model.singleton_methods.include?(method)
end

if value.is_a?(Array)
new_value = {}
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'

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use 2 (not 0) spaces for indenting an expression spanning multiple lines.

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

value.each_with_index do |v, i|
new_value[v] = i
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

new_value
def to_array(value)
if value.is_a?(Array)
value.map(&:to_s)
else
value
value.keys.map(&:to_s)
end
end
end
Expand Down
Loading