aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record/enum.rb
diff options
context:
space:
mode:
Diffstat (limited to 'activerecord/lib/active_record/enum.rb')
-rw-r--r--activerecord/lib/active_record/enum.rb92
1 files changed, 84 insertions, 8 deletions
diff --git a/activerecord/lib/active_record/enum.rb b/activerecord/lib/active_record/enum.rb
index c34fc086e2..f053372cfb 100644
--- a/activerecord/lib/active_record/enum.rb
+++ b/activerecord/lib/active_record/enum.rb
@@ -1,3 +1,5 @@
+require 'active_support/core_ext/object/deep_dup'
+
module ActiveRecord
# Declare an enum attribute where the values map to integers in the database,
# but can be queried by name. Example:
@@ -25,8 +27,10 @@ module ActiveRecord
# conversation.status # => nil
#
# Scopes based on the allowed values of the enum field will be provided
- # as well. With the above example, it will create an +active+ and +archived+
- # scope.
+ # as well. With the above example:
+ #
+ # Conversation.active
+ # Conversation.archived
#
# You can set the default value from the database declaration, like:
#
@@ -54,14 +58,27 @@ module ActiveRecord
# remove unused values, the explicit +Hash+ syntax should be used.
#
# In rare circumstances you might need to access the mapping directly.
- # The mappings are exposed through a constant with the attributes name:
+ # The mappings are exposed through a class method with the pluralized attribute
+ # name:
#
# Conversation.statuses # => { "active" => 0, "archived" => 1 }
#
- # Use that constant when you need to know the ordinal value of an enum:
+ # Use that class method when you need to know the ordinal value of an enum:
#
# Conversation.where("status <> ?", Conversation.statuses[:archived])
+ #
+ # Where conditions on an enum attribute must use the ordinal value of an enum.
module Enum
+ def self.extended(base) # :nodoc:
+ base.class_attribute(:defined_enums)
+ base.defined_enums = {}
+ end
+
+ def inherited(base) # :nodoc:
+ base.defined_enums = defined_enums.deep_dup
+ super
+ end
+
def enum(definitions)
klass = self
definitions.each do |name, values|
@@ -70,10 +87,12 @@ module ActiveRecord
name = name.to_sym
# def self.statuses statuses end
+ detect_enum_conflict!(name, name.to_s.pluralize, true)
klass.singleton_class.send(:define_method, name.to_s.pluralize) { enum_values }
_enum_methods_module.module_eval do
# def status=(value) self[:status] = statuses[value] end
+ klass.send(:detect_enum_conflict!, name, "#{name}=")
define_method("#{name}=") { |value|
if enum_values.has_key?(value) || value.blank?
self[name] = enum_values[value]
@@ -88,35 +107,92 @@ module ActiveRecord
}
# def status() statuses.key self[:status] end
+ klass.send(:detect_enum_conflict!, name, name)
define_method(name) { enum_values.key self[name] }
# def status_before_type_cast() statuses.key self[:status] end
+ klass.send(:detect_enum_conflict!, name, "#{name}_before_type_cast")
define_method("#{name}_before_type_cast") { enum_values.key self[name] }
pairs = values.respond_to?(:each_pair) ? values.each_pair : values.each_with_index
pairs.each do |value, i|
enum_values[value] = i
- # scope :active, -> { where status: 0 }
- klass.scope value, -> { klass.where name => i }
-
# def active?() status == 0 end
+ klass.send(:detect_enum_conflict!, name, "#{value}?")
define_method("#{value}?") { self[name] == i }
# def active!() update! status: :active end
+ klass.send(:detect_enum_conflict!, name, "#{value}!")
define_method("#{value}!") { update! name => value }
+
+ # scope :active, -> { where status: 0 }
+ klass.send(:detect_enum_conflict!, name, value, true)
+ klass.scope value, -> { klass.where name => i }
end
end
+ defined_enums[name.to_s] = enum_values
end
end
private
def _enum_methods_module
@_enum_methods_module ||= begin
- mod = Module.new
+ mod = Module.new do
+ private
+ def save_changed_attribute(attr_name, old)
+ if (mapping = self.class.defined_enums[attr_name.to_s])
+ value = _read_attribute(attr_name)
+ if attribute_changed?(attr_name)
+ if mapping[old] == value
+ clear_attribute_changes([attr_name])
+ end
+ else
+ if old != value
+ set_attribute_was(attr_name, mapping.key(old))
+ end
+ end
+ else
+ super
+ end
+ end
+ end
include mod
mod
end
end
+
+ ENUM_CONFLICT_MESSAGE = \
+ "You tried to define an enum named \"%{enum}\" on the model \"%{klass}\", but " \
+ "this will generate a %{type} method \"%{method}\", which is already defined " \
+ "by %{source}."
+
+ def detect_enum_conflict!(enum_name, method_name, klass_method = false)
+ if klass_method && dangerous_class_method?(method_name)
+ raise ArgumentError, ENUM_CONFLICT_MESSAGE % {
+ enum: enum_name,
+ klass: self.name,
+ type: 'class',
+ method: method_name,
+ source: 'Active Record'
+ }
+ elsif !klass_method && dangerous_attribute_method?(method_name)
+ raise ArgumentError, ENUM_CONFLICT_MESSAGE % {
+ enum: enum_name,
+ klass: self.name,
+ type: 'instance',
+ method: method_name,
+ source: 'Active Record'
+ }
+ elsif !klass_method && method_defined_within?(method_name, _enum_methods_module, Module)
+ raise ArgumentError, ENUM_CONFLICT_MESSAGE % {
+ enum: enum_name,
+ klass: self.name,
+ type: 'instance',
+ method: method_name,
+ source: 'another enum'
+ }
+ end
+ end
end
end