# SimpleEnum allows for cross-database, easy to use enum-like fields to be added to your
# ActiveRecord models. It does not rely on database specific column types like ENUM (MySQL),
# but instead on integer columns.
#
# Author:: Lukas Westermann
# Copyright:: Copyright (c) 2009 Lukas Westermann (Zurich, Switzerland)
# Licence:: MIT-Licence (http://www.opensource.org/licenses/mit-license.php)
#
# See the +as_enum+ documentation for more details.
require 'simple_enum/array_support'
require 'simple_enum/enum_hash'
require 'simple_enum/object_support'
require 'simple_enum/validation'
require 'simple_enum/version'
# Base module which gets included in ActiveRecord::Base. See documentation
# of +SimpleEnum::ClassMethods+ for more details.
module SimpleEnum
class << self
# Provides configurability to SimpleEnum, allows to override some defaults which are
# defined for all uses of +as_enum+. Most options from +as_enum+ are available, such as:
# * :prefix - Define a prefix, which is prefixed to the shortcut methods (e.g. ! and
# ?), if it's set to true the enumeration name is used as a prefix, else a custom
# prefix (symbol or string) (default is nil => no prefix)
# * :slim - If set to true no shortcut methods for all enumeration values are being generated, if
# set to :class only class-level shortcut methods are disabled (default is nil => they are generated)
# * :upcase - If set to +true+ the Klass.foos is named Klass.FOOS, why? To better suite some
# coding-styles (default is +false+ => downcase)
# * :whiny - Boolean value which if set to true will throw an ArgumentError
# if an invalid value is passed to the setter (e.g. a value for which no enumeration exists). if set to
# false no exception is thrown and the internal value is set to nil (default is true)
def default_options
@default_options ||= {
:whiny => true,
:upcase => false
}
end
def included(base) #:nodoc:
base.send :extend, ClassMethods
end
end
module ClassMethods
# Provides ability to create simple enumerations based on hashes or arrays, backed
# by integer columns (but not limited to integer columns).
#
# Columns are supposed to be suffixed by _cd, if not, use :column => 'the_column_name',
# so some example migrations:
#
# add_column :users, :gender_cd, :integer
# add_column :users, :status, :integer # and a custom column...
#
# and then in your model:
#
# class User < ActiveRecord::Base
# as_enum :gender, [:male, :female]
# end
#
# # or use a hash:
#
# class User < ActiveRecord::Base
# as_enum :status, { :active => 1, :inactive => 0, :archived => 2, :deleted => 3 }, :column => 'status'
# end
#
# Now it's possible to access the enumeration and the internally stored value like:
#
# john_doe = User.new
# john_doe.gender # => nil
# john_doe.gender = :male
# john_doe.gender # => :male
# john_doe.gender_cd # => 0
#
# And to make life a tad easier: a few shortcut methods to work with the enumeration are also created.
#
# john_doe.male? # => true
# john_doe.female? # => false
# john_doe.female! # => :female (set's gender to :female => gender_cd = 1)
# john_doe.male? # => false
#
# Sometimes it's required to access the db-backed values, like e.g. in a query:
#
# User.genders # => { :male => 0, :female => 1}, values hash
# User.genders(:male) # => 0, value access (via hash)
# User.female # => 1, direct access
# User.find :all, :conditions => { :gender_cd => User.female } # => [...], list with all women
#
# To access the key/value assocations in a helper like the select helper or similar use:
#
# <%= select(:user, :gender, User.genders.keys)
#
# The generated shortcut methods (like male? or female! etc.) can also be prefixed
# using the :prefix option. If the value is true, the shortcut methods are prefixed
# with the name of the enumeration.
#
# class User < ActiveRecord::Base
# as_enum :gender, [:male, :female], :prefix => true
# end
#
# jane_doe = User.new
# jane_doe.gender = :female # this is still as-is
# jane_doe.gender_cd # => 1, and so it this
#
# jane_doe.gender_female? # => true (instead of jane_doe.female?)
#
# It is also possible to supply a custom prefix.
#
# class Item < ActiveRecord::Base
# as_enum :status, [:inactive, :active, :deleted], :prefix => :state
# end
#
# item = Item.new(:status => :active)
# item.state_inactive? # => false
# Item.state_deleted # => 2
# Item.status(:deleted) # => 2, same as above...
#
# To disable the generation of the shortcut methods for all enumeration values, add :slim => true to
# the options.
#
# class Address < ActiveRecord::Base
# as_enum :canton, {:aargau => 'ag', ..., :wallis => 'vs', :zug => 'zg', :zurich => 'zh'}, :slim => true
# end
#
# home = Address.new(:canton => :zurich, :street => 'Bahnhofstrasse 1', ...)
# home.canton # => :zurich
# home.canton_cd # => 'zh'
# home.aargau! # throws NoMethodError: undefined method `aargau!'
# Address.aargau # throws NoMethodError: undefined method `aargau`
#
# This is especially useful if there are (too) many enumeration values, or these shortcut methods
# are not required.
#
# === Configuration options:
# * :column - Specifies a custom column name, instead of the default suffixed _cd column
# * :prefix - Define a prefix, which is prefixed to the shortcut methods (e.g. ! and
# ?), if it's set to true the enumeration name is used as a prefix, else a custom
# prefix (symbol or string) (default is nil => no prefix)
# * :slim - If set to true no shortcut methods for all enumeration values are being generated, if
# set to :class only class-level shortcut methods are disabled (default is nil => they are generated)
# * :upcase - If set to +true+ the Klass.foos is named Klass.FOOS, why? To better suite some
# coding-styles (default is +false+ => downcase)
# * :whiny - Boolean value which if set to true will throw an ArgumentError
# if an invalid value is passed to the setter (e.g. a value for which no enumeration exists). if set to
# false no exception is thrown and the internal value is set to nil (default is true)
def as_enum(enum_cd, values, options = {})
options = SimpleEnum.default_options.merge({ :column => "#{enum_cd}_cd" }).merge(options)
options.assert_valid_keys(:column, :whiny, :prefix, :slim, :upcase)
# convert array to hash...
values = SimpleEnum::EnumHash.new(values)
values_inverted = values.invert
# store info away
write_inheritable_attribute(:enum_definitions, {}) if enum_definitions.nil?
enum_definitions[enum_cd] = enum_definitions[options[:column]] = { :name => enum_cd, :column => options[:column], :options => options }
# generate getter
define_method("#{enum_cd}") do
id = read_attribute options[:column]
values_inverted[id]
end
# generate setter
define_method("#{enum_cd}=") do |new_value|
v = new_value.nil? ? nil : values[new_value.to_sym]
raise(ArgumentError, "Invalid enumeration value: #{new_value}") if (options[:whiny] and v.nil? and !new_value.nil?)
write_attribute options[:column], v
end
# allow access to defined values hash, e.g. in a select helper or finder method.
self_name = enum_cd.to_s.pluralize
self_name.upcase! if options[:upcase]
class_variable_set :"@@SE_#{self_name.upcase}", values
class_eval(<<-EOM, __FILE__, __LINE__ + 1)
def self.#{self_name}(sym = nil)
return class_variable_get(:@@SE_#{self_name.upcase}) if sym.nil?
class_variable_get(:@@SE_#{self_name.upcase})[sym]
end
def self.#{self_name}_for_select(&block)
self.#{self_name}.map do |k,v|
[block_given? ? yield(k,v) : self.human_enum_name(#{self_name.inspect}, k), v]
end.sort { |a,b| a[1] <=> b[1] }
end
EOM
# only create if :slim is not defined
if options[:slim] != true
# create both, boolean operations and *bang* operations for each
# enum "value"
prefix = options[:prefix] && "#{options[:prefix] == true ? enum_cd : options[:prefix]}_"
values.each do |k,code|
sym = k.to_enum_sym
define_method("#{prefix}#{sym}?") do
code == read_attribute(options[:column])
end
define_method("#{prefix}#{sym}!") do
write_attribute options[:column], code
sym
end
# allow class access to each value
unless options[:slim] === :class
metaclass.send(:define_method, "#{prefix}#{sym}", Proc.new { |*args| args.first ? k : code })
end
end
end
end
include Validation
def human_enum_name(enum, key, options = {})
defaults = self_and_descendants_from_active_record.map { |klass| :"#{klass.name.underscore}.#{enum}.#{key}" }
defaults << :"#{enum}.#{key}"
defaults << options.delete(:default) if options[:default]
defaults << "#{key}".humanize
options[:count] ||= 1
I18n.translate(defaults.shift, options.merge(:default => defaults.flatten, :scope => [:activerecord, :enums]))
end
protected
# Returns enum definitions as defined by each call to
# +as_enum+.
def enum_definitions
read_inheritable_attribute(:enum_definitions)
end
end
end
# Tie stuff together and load translations if ActiveRecord is defined
if Object.const_defined?('ActiveRecord')
Object.send(:include, SimpleEnum::ObjectSupport)
Array.send(:include, SimpleEnum::ArraySupport)
ActiveRecord::Base.send(:include, SimpleEnum)
I18n.load_path << File.join(File.dirname(__FILE__), '..', 'locales', 'en.yml')
end