Refactor enum to be defined in terms of the attributes API

In addition to cleaning up the implementation, this allows type casting
behavior to be applied consistently everywhere. (#where for example). A
good example of this was the previous need for handling value to key
conversion in the setter, because the number had to be passed to `where`
directly. This is no longer required, since we can just pass the string
along to where. (It's left around for backwards compat)

Fixes #18387
This commit is contained in:
Sean Griffin 2015-02-11 14:56:26 -07:00
parent 5e0b555b45
commit c51f9b61ce
3 changed files with 60 additions and 45 deletions

@ -1,3 +1,8 @@
* Have `enum` perform type casting consistently with the rest of Active
Record, such as `where`.
*Sean Griffin*
* `scoping` no longer pollutes the current scope of sibling classes when using
STI. e.x.

@ -79,6 +79,37 @@ def inherited(base) # :nodoc:
super
end
class EnumType < Type::Value
def initialize(name, mapping)
@name = name
@mapping = mapping
end
def type_cast_from_user(value)
return if value.blank?
if mapping.has_key?(value)
value.to_s
elsif mapping.has_value?(value)
mapping.key(value)
else
raise ArgumentError, "'#{value}' is not a valid #{name}"
end
end
def type_cast_from_database(value)
mapping.key(value)
end
def type_cast_for_database(value)
mapping.fetch(value, value)
end
protected
attr_reader :name, :mapping
end
def enum(definitions)
klass = self
definitions.each do |name, values|
@ -90,37 +121,19 @@ def enum(definitions)
detect_enum_conflict!(name, name.to_s.pluralize, true)
klass.singleton_class.send(:define_method, name.to_s.pluralize) { enum_values }
detect_enum_conflict!(name, name)
detect_enum_conflict!(name, "#{name}=")
attribute name, EnumType.new(name, 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]
elsif enum_values.has_value?(value)
# Assigning a value directly is not a end-user feature, hence it's not documented.
# This is used internally to make building objects from the generated scopes work
# as expected, i.e. +Conversation.archived.build.archived?+ should be true.
self[name] = value
else
raise ArgumentError, "'#{value}' is not a valid #{name}"
end
}
# 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
# def active?() status == 0 end
klass.send(:detect_enum_conflict!, name, "#{value}?")
define_method("#{value}?") { self[name] == i }
define_method("#{value}?") { self[name] == value.to_s }
# def active!() update! status: :active end
klass.send(:detect_enum_conflict!, name, "#{value}!")
@ -128,7 +141,7 @@ def enum(definitions)
# scope :active, -> { where status: 0 }
klass.send(:detect_enum_conflict!, name, value, true)
klass.scope value, -> { klass.where name => i }
klass.scope value, -> { klass.where name => value }
end
end
defined_enums[name.to_s] = enum_values
@ -138,25 +151,7 @@ def enum(definitions)
private
def _enum_methods_module
@_enum_methods_module ||= begin
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
mod = Module.new
include mod
mod
end

@ -26,6 +26,17 @@ class EnumTest < ActiveRecord::TestCase
assert_equal @book, Book.unread.first
end
test "build from scope" do
assert Book.proposed.build.proposed?
refute Book.proposed.build.written?
assert Book.where(status: Book.statuses[:proposed]).build.proposed?
end
test "find via where" do
assert_equal @book, Book.where(status: "proposed").first
refute_equal @book, Book.where(status: "written").first
end
test "update by declaration" do
@book.written!
assert @book.written?
@ -161,7 +172,11 @@ class EnumTest < ActiveRecord::TestCase
end
test "_before_type_cast returns the enum label (required for form fields)" do
assert_equal "proposed", @book.status_before_type_cast
if @book.status_came_from_user?
assert_equal "proposed", @book.status_before_type_cast
else
assert_equal "proposed", @book.status
end
end
test "reserved enum names" do