# 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