From 386a773be79aaca776853fbf0e89903df3a5546a Mon Sep 17 00:00:00 2001 From: Elliot Winkler Date: Fri, 26 Jan 2018 01:12:57 -0600 Subject: [PATCH] Add with_prefix and with_suffix to define_enum_for In Rails 5, the `enum` macro received two new options: `_prefix` and `_suffix`. These options change the names of the methods that are generated from the given enum values. This commit adds qualifiers to the `define_enum_for` matcher so that they can be tested. --- .../active_record/define_enum_for_matcher.rb | 118 +++++- .../unit/helpers/active_record_versions.rb | 4 + .../define_enum_for_matcher_spec.rb | 349 +++++++++++++++++- 3 files changed, 465 insertions(+), 6 deletions(-) diff --git a/lib/shoulda/matchers/active_record/define_enum_for_matcher.rb b/lib/shoulda/matchers/active_record/define_enum_for_matcher.rb index 7ac46a9cd..09e1da207 100644 --- a/lib/shoulda/matchers/active_record/define_enum_for_matcher.rb +++ b/lib/shoulda/matchers/active_record/define_enum_for_matcher.rb @@ -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] # @@ -98,6 +147,23 @@ 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( @@ -121,6 +187,16 @@ def with(expected_enum_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 @@ -128,7 +204,11 @@ 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 @@ -168,6 +248,10 @@ 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 @@ -238,6 +322,38 @@ 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)| diff --git a/spec/support/unit/helpers/active_record_versions.rb b/spec/support/unit/helpers/active_record_versions.rb index d9d30e3a0..313c7b999 100644 --- a/spec/support/unit/helpers/active_record_versions.rb +++ b/spec/support/unit/helpers/active_record_versions.rb @@ -9,6 +9,10 @@ def active_record_version Tests::Version.new(::ActiveRecord::VERSION::STRING) 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 diff --git a/spec/unit/shoulda/matchers/active_record/define_enum_for_matcher_spec.rb b/spec/unit/shoulda/matchers/active_record/define_enum_for_matcher_spec.rb index f7f3229f3..f72bb334a 100644 --- a/spec/unit/shoulda/matchers/active_record/define_enum_for_matcher_spec.rb +++ b/spec/unit/shoulda/matchers/active_record/define_enum_for_matcher_spec.rb @@ -304,30 +304,361 @@ def self.statuses; end end end + if active_record_enum_supports_prefix_and_suffix? + context 'qualified with #with_prefix' do + context 'when the prefix is explicit' do + context 'if the attribute was not defined with a prefix' do + it 'rejects with an appropriate failure message' do + record = build_record_with_array_values( + model_name: 'Example', + attribute_name: :attr, + column_type: :integer, + values: [:active, :archived], + ) + + assertion = lambda do + expect(record). + to define_enum_for(:attr). + with_values([:active, :archived]). + with_prefix(:foo) + end + + message = word_wrap(<<~MESSAGE) + Expected Example to define :attr as an enum, backed by an integer, + using a prefix of :foo, with possible values ‹[:active, + :archived]›. However, it was defined with either a different + prefix or none at all. + MESSAGE + + expect(&assertion).to fail_with_message(message) + end + end + + context 'if the attribute was defined with a different prefix' do + it 'rejects with an appropriate failure message' do + record = build_record_with_array_values( + model_name: 'Example', + attribute_name: :attr, + column_type: :integer, + values: [:active, :archived], + prefix: :foo, + ) + + assertion = lambda do + expect(record). + to define_enum_for(:attr). + with_values([:active, :archived]). + with_prefix(:bar) + end + + message = word_wrap(<<~MESSAGE) + Expected Example to define :attr as an enum, backed by an integer, + using a prefix of :bar, with possible values ‹[:active, + :archived]›. However, it was defined with either a different + prefix or none at all. + MESSAGE + + expect(&assertion).to fail_with_message(message) + end + end + + context 'if the attribute was defined with the same prefix' do + it 'accepts' do + record = build_record_with_array_values( + model_name: 'Example', + attribute_name: :attr, + values: [:active, :archived], + prefix: :foo, + ) + + expect(record). + to define_enum_for(:attr). + with_values([:active, :archived]). + with_prefix(:foo) + end + end + end + + context 'when the prefix is implicit' do + context 'if the attribute was not defined with a prefix' do + it 'rejects with an appropriate failure message' do + record = build_record_with_array_values( + model_name: 'Example', + attribute_name: :attr, + column_type: :integer, + values: [:active, :archived], + ) + + assertion = lambda do + expect(record). + to define_enum_for(:attr). + with_values([:active, :archived]). + with_prefix + end + + message = word_wrap(<<~MESSAGE) + Expected Example to define :attr as an enum, backed by an integer, + using a prefix of :attr, with possible values ‹[:active, + :archived]›. However, it was defined with either a different + prefix or none at all. + MESSAGE + + expect(&assertion).to fail_with_message(message) + end + end + + context 'if the attribute was defined with a prefix' do + it 'accepts' do + record = build_record_with_array_values( + model_name: 'Example', + attribute_name: :attr, + values: [:active, :archived], + prefix: true, + ) + + expect(record). + to define_enum_for(:attr). + with_values([:active, :archived]). + with_prefix + end + end + end + end + + context 'qualified with #with_suffix' do + context 'when the suffix is explicit' do + context 'if the attribute was not defined with a suffix' do + it 'rejects with an appropriate failure message' do + record = build_record_with_array_values( + model_name: 'Example', + attribute_name: :attr, + column_type: :integer, + values: [:active, :archived], + ) + + assertion = lambda do + expect(record). + to define_enum_for(:attr). + with_values([:active, :archived]). + with_suffix(:foo) + end + + message = word_wrap(<<~MESSAGE) + Expected Example to define :attr as an enum, backed by an integer, + using a suffix of :foo, with possible values ‹[:active, + :archived]›. However, it was defined with either a different + suffix or none at all. + MESSAGE + + expect(&assertion).to fail_with_message(message) + end + end + + context 'if the attribute was defined with a different suffix' do + it 'rejects with an appropriate failure message' do + record = build_record_with_array_values( + model_name: 'Example', + attribute_name: :attr, + column_type: :integer, + values: [:active, :archived], + suffix: :foo, + ) + + assertion = lambda do + expect(record). + to define_enum_for(:attr). + with_values([:active, :archived]). + with_suffix(:bar) + end + + message = word_wrap(<<~MESSAGE) + Expected Example to define :attr as an enum, backed by an integer, + using a suffix of :bar, with possible values ‹[:active, + :archived]›. However, it was defined with either a different + suffix or none at all. + MESSAGE + + expect(&assertion).to fail_with_message(message) + end + end + + context 'if the attribute was defined with the same suffix' do + it 'accepts' do + record = build_record_with_array_values( + model_name: 'Example', + attribute_name: :attr, + values: [:active, :archived], + suffix: :foo, + ) + + expect(record). + to define_enum_for(:attr). + with_values([:active, :archived]). + with_suffix(:foo) + end + end + end + + context 'when the suffix is implicit' do + context 'if the attribute was not defined with a suffix' do + it 'rejects with an appropriate failure message' do + record = build_record_with_array_values( + model_name: 'Example', + attribute_name: :attr, + column_type: :integer, + values: [:active, :archived], + ) + + assertion = lambda do + expect(record). + to define_enum_for(:attr). + with_values([:active, :archived]). + with_suffix + end + + message = word_wrap(<<~MESSAGE) + Expected Example to define :attr as an enum, backed by an integer, + using a suffix of :attr, with possible values ‹[:active, + :archived]›. However, it was defined with either a different + suffix or none at all. + MESSAGE + + expect(&assertion).to fail_with_message(message) + end + end + + context 'if the attribute was defined with a suffix' do + it 'accepts' do + record = build_record_with_array_values( + model_name: 'Example', + attribute_name: :attr, + values: [:active, :archived], + suffix: true, + ) + + expect(record). + to define_enum_for(:attr). + with_values([:active, :archived]). + with_suffix + end + end + end + end + + context 'qualified with both #with_prefix and #with_suffix' do + context 'if the attribute was not defined with a different prefix' do + it 'rejects with an appropriate failure message' do + record = build_record_with_array_values( + model_name: 'Example', + attribute_name: :attr, + column_type: :integer, + values: [:active, :archived], + prefix: :foo, + suffix: :bar, + ) + + assertion = lambda do + expect(record). + to define_enum_for(:attr). + with_values([:active, :archived]). + with_prefix(:whatever). + with_suffix(:bar) + end + + message = word_wrap(<<~MESSAGE) + Expected Example to define :attr as an enum, backed by an integer, + using a prefix of :whatever and a suffix of :bar, with possible + values ‹[:active, :archived]›. However, it was defined with either + a different prefix, a different suffix, or neither one at all. + MESSAGE + + expect(&assertion).to fail_with_message(message) + end + + context 'if the attribute was defined with a different suffix' do + it 'rejects with an appropriate failure message' do + record = build_record_with_array_values( + model_name: 'Example', + attribute_name: :attr, + column_type: :integer, + values: [:active, :archived], + prefix: :foo, + suffix: :bar, + ) + + assertion = lambda do + expect(record). + to define_enum_for(:attr). + with_values([:active, :archived]). + with_prefix(:foo). + with_suffix(:whatever) + end + + message = word_wrap(<<~MESSAGE) + Expected Example to define :attr as an enum, backed by an integer, + using a prefix of :foo and a suffix of :whatever, with possible + values ‹[:active, :archived]›. However, it was defined with + either a different prefix, a different suffix, or neither one at + all. + MESSAGE + + expect(&assertion).to fail_with_message(message) + end + end + + context 'if the attribute was defined with the same prefix and suffix' do + it 'accepts' do + record = build_record_with_array_values( + model_name: 'Example', + attribute_name: :attr, + values: [:active, :archived], + prefix: :foo, + suffix: :bar, + ) + + expect(record). + to define_enum_for(:attr). + with_values([:active, :archived]). + with_prefix(:foo). + with_suffix(:bar) + end + end + end + end + end + def build_record_with_array_values( model_name: 'Example', attribute_name: :attr, column_type: :integer, - values: ['published', 'unpublished', 'draft'] + values: ['published', 'unpublished', 'draft'], + prefix: false, + suffix: false ) build_record_with_enum_attribute( model_name: model_name, attribute_name: attribute_name, column_type: column_type, values: values, + prefix: prefix, + suffix: suffix, ) end def build_record_with_hash_values( model_name: 'Example', attribute_name: :attr, - values: { active: 0, archived: 1 } + values: { active: 0, archived: 1 }, + prefix: false, + suffix: false ) build_record_with_enum_attribute( model_name: model_name, attribute_name: attribute_name, column_type: :integer, values: values, + prefix: prefix, + suffix: suffix, ) end @@ -335,13 +666,21 @@ def build_record_with_enum_attribute( model_name:, attribute_name:, column_type:, - values: + values:, + prefix: false, + suffix: false ) model = define_model( model_name, - attribute_name => column_type, + attribute_name => { type: column_type }, ) - model.enum(attribute_name => values) + + if active_record_enum_supports_prefix_and_suffix? + model.enum(attribute_name => values, _prefix: prefix, _suffix: suffix) + else + model.enum(attribute_name => values) + end + model.new end