Method: SimpleEnum::ClassMethods#as_enum

Defined in:
lib/simple_enum.rb

#as_enum(enum_cd, values, options = {}) ⇒ Object

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. <symbol>! and <symbol>?), 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)



153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
# File 'lib/simple_enum.rb', line 153

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)

  metaclass = (class << self; self; end)

  # 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.blank? ? nil : values[new_value.to_sym]
    raise(ArgumentError, "Invalid enumeration value: #{new_value}") if (options[:whiny] and v.nil? and !new_value.blank?)
    write_attribute options[:column], v
  end

  # allow access to defined values hash, e.g. in a select helper or finder method.      
  attr_name = enum_cd.to_s.pluralize
  enum_attr = :"#{attr_name.downcase}_enum_hash"
  write_inheritable_attribute(enum_attr, values)
  
  class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
    def self.#{attr_name}(*args)
      return read_inheritable_attribute(#{enum_attr.inspect}) if args.first.nil?
      return read_inheritable_attribute(#{enum_attr.inspect})[args.first] if args.size == 1
      args.inject([]) { |ary, sym| ary << read_inheritable_attribute(#{enum_attr.inspect})[sym]; ary }
    end
    
    def self.#{attr_name}_for_select(attr = :key, &block)
      self.#{attr_name}.map do |k,v|
        [block_given? ? yield(k,v) : self.human_enum_name(#{attr_name.inspect}, k), attr == :value ? v : k]
      end
    end
  RUBY

  # 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