From 307d50292dac93279bec2eae329b107b23e81ca5 Mon Sep 17 00:00:00 2001 From: Ryuta Kamizono Date: Fri, 1 Jan 2021 14:34:06 +0900 Subject: [PATCH] Should not fail if mangling enum names collision Just skip defining mangling enum methods in that case. Fixes #40804. --- activerecord/lib/active_record/enum.rb | 82 ++++++++++++++---------- activerecord/test/cases/enum_test.rb | 11 ++++ activerecord/test/fixtures/computers.yml | 2 + activerecord/test/schema/schema.rb | 1 + 4 files changed, 62 insertions(+), 34 deletions(-) diff --git a/activerecord/lib/active_record/enum.rb b/activerecord/lib/active_record/enum.rb index f41c429ce1..2e619b1b5f 100644 --- a/activerecord/lib/active_record/enum.rb +++ b/activerecord/lib/active_record/enum.rb @@ -158,8 +158,6 @@ module ActiveRecord end def enum(definitions) - klass = self - enum_prefix = definitions.delete(:_prefix) enum_suffix = definitions.delete(:_suffix) enum_scopes = definitions.delete(:_scopes) @@ -187,55 +185,71 @@ module ActiveRecord EnumType.new(attr, enum_values, subtype) end + value_method_names = [] _enum_methods_module.module_eval do + enum_prefix = name if enum_prefix == true + prefix = "#{enum_prefix}_" if enum_prefix + + enum_suffix = name if enum_suffix == true + suffix = "_#{enum_suffix}" if enum_suffix + pairs = values.respond_to?(:each_pair) ? values.each_pair : values.each_with_index - value_method_names = [] pairs.each do |label, value| - if enum_prefix == true - prefix = "#{name}_" - elsif enum_prefix - prefix = "#{enum_prefix}_" - end - if enum_suffix == true - suffix = "_#{name}" - elsif enum_suffix - suffix = "_#{enum_suffix}" - end - - method_friendly_label = label.to_s.gsub(/[\W&&[:ascii:]]+/, "_") - value_method_name = "#{prefix}#{method_friendly_label}#{suffix}" - value_method_names << value_method_name - enum_values[label] = value label = label.to_s + enum_values[label] = value - # def active?() status == "active" end - klass.send(:detect_enum_conflict!, name, "#{value_method_name}?") - define_method("#{value_method_name}?") { self[attr] == label } + value_method_name = "#{prefix}#{label}#{suffix}" + value_method_names << value_method_name + define_enum_methods(name, value_method_name, label, enum_scopes) - # def active!() update!(status: 0) end - klass.send(:detect_enum_conflict!, name, "#{value_method_name}!") - define_method("#{value_method_name}!") { update!(attr => value) } + method_friendly_label = label.gsub(/[\W&&[:ascii:]]+/, "_") + value_method_alias = "#{prefix}#{method_friendly_label}#{suffix}" - # scope :active, -> { where(status: 0) } - # scope :not_active, -> { where.not(status: 0) } - if enum_scopes != false - klass.send(:detect_enum_conflict!, name, value_method_name, true) - klass.scope value_method_name, -> { where(attr => value) } - - klass.send(:detect_enum_conflict!, name, "not_#{value_method_name}", true) - klass.scope "not_#{value_method_name}", -> { where.not(attr => value) } + if value_method_alias != value_method_name && !value_method_names.include?(value_method_alias) + value_method_names << value_method_alias + define_enum_methods(name, value_method_alias, label, enum_scopes) end end - klass.send(:detect_negative_enum_conditions!, value_method_names) if enum_scopes != false end + detect_negative_enum_conditions!(value_method_names) if enum_scopes != false enum_values.freeze end end private + class EnumMethods < Module # :nodoc: + def initialize(klass) + @klass = klass + end + + private + attr_reader :klass + + def define_enum_methods(name, value_method_name, label, enum_scopes) + # def active?() status == "active" end + klass.send(:detect_enum_conflict!, name, "#{value_method_name}?") + define_method("#{value_method_name}?") { self[name] == label } + + # def active!() update!(status: 0) end + klass.send(:detect_enum_conflict!, name, "#{value_method_name}!") + define_method("#{value_method_name}!") { update!(name => label) } + + # scope :active, -> { where(status: 0) } + # scope :not_active, -> { where.not(status: 0) } + if enum_scopes != false + klass.send(:detect_enum_conflict!, name, value_method_name, true) + klass.scope value_method_name, -> { where(name => label) } + + klass.send(:detect_enum_conflict!, name, "not_#{value_method_name}", true) + klass.scope "not_#{value_method_name}", -> { where.not(name => label) } + end + end + end + private_constant :EnumMethods + def _enum_methods_module @_enum_methods_module ||= begin - mod = Module.new + mod = EnumMethods.new(self) include mod mod end diff --git a/activerecord/test/cases/enum_test.rb b/activerecord/test/cases/enum_test.rb index 6451125b9d..833bfa82b6 100644 --- a/activerecord/test/cases/enum_test.rb +++ b/activerecord/test/cases/enum_test.rb @@ -657,6 +657,17 @@ class EnumTest < ActiveRecord::TestCase assert_not_predicate book, :🇪🇸? end + test "mangling collision for enum names" do + klass = Class.new(ActiveRecord::Base) do + self.table_name = "computers" + enum timezone: [:"Etc/GMT+1", :"Etc/GMT-1"] + end + + computer = klass.public_send(:"Etc/GMT+1").build + assert_predicate computer, :"Etc/GMT+1?" + assert_not_predicate computer, :"Etc/GMT-1?" + end + test "enum logs a warning if auto-generated negative scopes would clash with other enum names" do old_logger = ActiveRecord::Base.logger logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new diff --git a/activerecord/test/fixtures/computers.yml b/activerecord/test/fixtures/computers.yml index ad5ae2ec71..aa2a49688e 100644 --- a/activerecord/test/fixtures/computers.yml +++ b/activerecord/test/fixtures/computers.yml @@ -3,8 +3,10 @@ workstation: system: 'Linux' developer: 1 extendedWarranty: 1 + timezone: 1 laptop: system: 'MacOS 1' developer: 1 extendedWarranty: 1 + timezone: 1 diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index 31de9c9fc3..d9843690ed 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -265,6 +265,7 @@ ActiveRecord::Schema.define do t.string :system t.integer :developer, null: false t.integer :extendedWarranty, null: false + t.integer :timezone end create_table :computers_developers, id: false, force: true do |t|