Skip to content

Commit

Permalink
Re-factored acts_as enum out of core_ext for ActiveRecord. Really mes…
Browse files Browse the repository at this point in the history
…sy that way. Added HashEnums. New support for Hash-based enums bound to a given attribute.
  • Loading branch information
rahmal committed Jan 31, 2010
1 parent 0ef87db commit 7ce4650
Show file tree
Hide file tree
Showing 4 changed files with 273 additions and 10 deletions.
8 changes: 8 additions & 0 deletions .gitignore
@@ -0,0 +1,8 @@
*.iws
*.ipr
*.iml
*.swp
*.*~
.rakeTasks
.idea
*.log
207 changes: 207 additions & 0 deletions lib/acts_as_enum.rb
@@ -0,0 +1,207 @@
##
#
# Copyright (c) 2009 Rahmal Conda <rahmal@gmail.com>
#
# Extends ActiveRecord::Base to create enumerations
# from lookup tables when acts_as_enum is declared.
# All enumerations are cached, so this is a solution
# for relative small tables. Mainly, lookup tables
# such as state, zip code, and others that might be
# used in similar id/name patterns within web apps.
#
# Class accessors:
#
# ## id_attribute ##
# Custom id method, use this if you want to specify
# an id column that is not called 'id'. By default
# this code expects ar objects based on lookup
# tables with id/name column pairs.
#
# ## name_attribute ##
# Custom name method, use this if you want to specify
# and name column that is not called 'name'. By default
# this code expects ar objects based on lookup tables
# with id/name column pairs.
#
# ## primary_key ##
# Use this if the id column is not the primary key.
# By default it is set to true, and assumes the id
# attribute is the primary key.
#
# ## prefix ##
# Use this to specify a prefix when naming the enum
# constants. By default the names are used. i.e.
# CALIFORNIA = State.new(:id => 'CA', :name => 'California')
#
# But if you wish to create a constant for a table like
# zip_codes, you can use a prefix, i.e.
# ZIP60601 = ZipCode.new(:id => 60601, :name => '60601', :city ...)
#
# ## enums ##
# An array of all the instances of the created enums.
#
module ActsAsEnum

module MacroMethods

def acts_as_enum id_attr = :id, name_attr = :name, options = {}
cattr_accessor :id_attribute, :name_attribute,
:enums, :primary_key, :prefix

id_attribute, name_attribute = id_attr, name_attr

primary_key = options[:primary_key].nil? ? true : options[:primary_key] # default to true
prefix = options[:prefix] # use specified prefix on name, but strip it on search.

enums = []
@@enum_by_id = {} # not accessible outside the class
@@enum_by_nm = {} # this one too

rows = find(:all)
rows.each do |row|
id = row.send(self.id_attribute)
name = row.send(name_attribute)
id_is_name = false

if name.blank? # cound have no name i.e. ZipCode
name = row.send(id_attribute)
id_is_name = true
end

# Still no name? Nothing we can do.
raise ArgumentError, "Unable to retrieve suitable name" if name.blank?

make_constant name, prefix # i.e. State::ILLINOIS or PhoneType::HOME

# make enums from id as well, i.e. State::IL
# use prefix for numeric ids, i.e. ZipCode::ZIP60601
# if id is name, then enum is already created.
make_constant id, prefix if options[:make_id_enum] && !id_is_name

# Cache the rows for access and searching
self.enums << row
@@enums_by_id[scrub(id)] = row
@@enums_by_nm[scrub(name)] = row
end

# No changing these!
self.enums.freeze
@@enums_by_id.freeze
@@enums_by_nm.freeze

include ActsAsEnum::InstanceMethods
extend ActsAsEnum::ClassMethods
end

# Always returns false: class does not become an enum without calling acts_as_enum.
def enum?
false
end

private # helper methods

def make_constant name, prefix=nil
name = prefix + name.to_s unless prefix.blank?
name = scrub(name)
class_eval "#{name.upcase} = row unless defined? #{name.upcase}"
end

def scrub name
name.to_s.gsub(/\s/, '_').gsub(/\W/, '').downcase
end

end

module ClassMethods

def find_by_id(id)
@@enums_by_id[id.to_s.downcase]
end

def find_by_name(name)
@@enums_by_nm[name.to_s.downcase]
end

def [] enum
find_by_id(enum) || find_by_id(enum)
end

# Always returns true: This class is an enum since it
# has included this method by calling acts_as_enum
def enum?
true
end

# Does the given obj exist in the cache of enums for this class?
def exists? obj
not [obj].nil?
end
alias_method :exist?, :exists?
alias_method :include?, :exists?

# Is the id column the primary key?
def primary_key?
self.primary_key
end

# All the values for enums given id attribute
# i.e. State.ids => ['ny', 'il', 'ca' ...] or EmailType.ids => [1, 2, 3, ...]
def ids
@@enums_by_id.keys
end

# All the values for enums given name attribute i.e. PhoneType.names => ['home', 'work', 'fax' ...]
def names
@@enums_by_nm.keys
end

def to_select_options
enums.map do |enum|
[enum.id_value, enum.name_value]
end
end

end

module InstanceMethods

# Whether or not this enum is equal to the given value. If the value is
# an ActiveRecord then the default equality comparator is used.
# Otherwise, compare against the id and/or name attributes.
# i.e EmailType::PERSONAL == 'peronal' => true
# EmailType::WORK == EmailType[:personal] => fale
def ==(other)
if other.nil? || other.is_a?(self.class)
super
elsif other.is_a?(Integer) && self.class.primary_key?
other == self.send(self.class.id_attribute)
else
other = other.to_s.downcase
id = id_value.to_s.downcase
nm = self.send(self.class.name_attribute).to_s.downcase
other == id || othe == nm
end
end

# Returns the value from the column specified as the id_attribute,
# even if the column is not self.id
def id_value
send(self.class.id_attribute)
end

# Returns the value from the column specified as the name_attribute,
# even if the column is not self.name
def name_value
send(self.class.name_attribute)
end

# String representation of this enum instance, i.e. State::NEW_YORK.to_s => "New York"
def to_s
name_value.titleize
end

end

end
ActiveRecord::Base.class_eval { extend ActsAsEnum::MacroMethods }

10 changes: 0 additions & 10 deletions lib/core_ext/ar_base.rb
Expand Up @@ -59,16 +59,6 @@ def self.acts_as_enum *args
@@enums << name @@enums << name
end end
@@enums.freeze @@enums.freeze


class_eval do

define_method :[] do

end


end # class_eval
end end


def self.[] index def self.[] index
Expand Down
58 changes: 58 additions & 0 deletions lib/has_enums.rb
@@ -0,0 +1,58 @@
##
# Extends Object to add support to create
# enumerations from a Hash or Yaml file
# This is a great solution when a lookup
# table like pattern would be useful, but
# perhaps a bit weighty for a given situation.
#
# For example, an address table may have a
# type field that has several valid values
# i.e. home, office, etc. Enumerations for
# those values can be declared as a hash,
# or even stored in a yaml file, and then
# calling has_enums for that field will
# create accessors that ensure only those
# values can used. It also creates special
# validation and comparator methods for
# the field.
#
# This can be use by ActiveRecord and
# plain ruby objects.
#
# It also has support for creating select
# options for bound attribute.
#
# Examples:
#
# # Values can be a simple array
# has_enums :address_type,
# :values => ['home', 'office', 'shipping', 'billing']
#
# # Values can be a hash
# has_enums :address_type,
# :values => [1 => 'Home', 2 => 'Office', 3 => 'Shipping', 4 => 'billing']
#
# # Or Specify a yaml to get values from
# has_enums :address_type,
# :file => "#{RAILS_ROOT}/config/enums.yml",
# :key => :address_enums # Optional: Defaults to attribute name.
#
module HasEnums

module MacroMethods

def has_enums field, options = {}

include HasEnums::InstanceMethods
extend HasEnums::ClassMethods
end
end

module ClassMethods
end

module InstanceMethods
end

end
Object.class_eval { extend HasEnums::MacroMethods }

0 comments on commit 7ce4650

Please sign in to comment.