= SimpleEnum - unobtrusive enum-like fields for ActiveRecord

A Rails plugin which brings easy-to-use enum-like functionality to
ActiveRecord models.

*Note*: a recent search on github for `enum` turned out, that there are many, many similar solutions.
Yet, none seem to provide so many options, but I may be biased ;)

== Quick start

Add this to a model:

    class User < ActiveRecord::Base
      as_enum :gender, {:female => 1, :male => 0}
    end

Then create the new column using migrations:

    class AddGenderColumnToUser < ActiveRecord::Migration
      def self.up
        add_column :users, :gender_cd, :integer
      end

      def self.down
        remove_column :users, :gender_cd
      end
    end

*Done*. Now it's possible to pull some neat tricks on the new column, yet
the original db column (+gender_cd+) is still intact and not touched by
any fancy metaclass or similar.

    jane = User.new
    jane.gender = :female
    jane.female?   # => true
    jane.male?     # => false
    jane.gender    # => :female
    jane.gender_cd # => 1
    
Easily switch to another value using the bang methods.
    
    joe = User.new
    joe.male!     # => :male
    joe.gender    # => :male
    joe.gender_cd # => 0

There are even some neat tricks at class level, which might be
useful when creating queries, displaying option elements or similar:

    User.genders            # => { :male => 0, :female => 1 }
    User.genders(:male)     # => 0, same as User.male
    User.female             # => 1
    User.genders.female     # => 1, same as User.female or User.genders(:female)

== Wait, there's more!
* Too tired of always adding the integer values? Try:

        class User < ActiveRecord::Base
          as_enum :status, [:deleted, :active, :disabled] # translates to :deleted => 0, :active => 1, :disabled => 2
        end

  *Disclaimer*: if you _ever_ decide to reorder this array, beaware that any previous mapping is lost. So it's recommended
  to create mappings (that might change) using hashes instead of arrays. For stuff like gender it might be probably perfectly
  fine to use arrays though.
* Maybe you've columns named differently than the proposed <tt>{column}_cd</tt> naming scheme, feel free to use any column name
  by providing an option:

        class User < ActiveRecord::Base
          as_enum :gender, [:male, :female], :column => 'sex'
        end
        
* To make it easier to create dropdowns with values use:

        <%= select(:user, :gender, User.genders.keys) %>
        
* It's possible to validate the internal enum values, just like any other ActiveRecord validation:

        class User < ActiveRecord::Base
          as_enum :gender, [:male, :female]
          validates_as_enum :gender
        end

  All common options like <tt>:if</tt>, <tt>:unless</tt>, <tt>:allow_nil</tt> and <tt>:message</tt> are supported, because it just works within
  the standard <tt>validates_each</tt>-loop. This validation method does not check the value of <tt>user.gender</tt>, but
  instead the value of <tt>@user.gender_cd</tt>.
* If the shortcut methods (like <tt><symbol>?</tt>, <tt><symbol>!</tt> or <tt>Klass.<symbol></tt>) conflict with something in your class, it's possible to
  define a prefix:
  
        class User < ActiveRecord::Base
          as_enum :gender, [:male, :female], :prefix => true
        end

        jane = User.new :gender => :female
        jane.gender_female? # => true
        User.gender_female  # => 1, this also works on the class methods
        
  The <tt>:prefix</tt> option not only takes a boolean value as an argument, but instead can also be supplied a custom
  prefix (i.e. any string or symbol), so with <tt>:prefix => 'foo'</tt> all shortcut methods would look like: <tt>foo_<symbol>...</tt>
  *Note*: if the <tt>:slim => true</tt> is defined, this option has no effect whatsoever (because no shortcut methods are generated).
* Sometimes it might be useful to disable the generation of the shortcut methods (<tt><symbol>?</tt>, <tt><symbol>!</tt> and <tt>Klass.<symbol></tt>), to do so just add the option <tt>:slim => true</tt>:
  
        class User < ActiveRecord::Base
          as_enum :gender, [:male, :female], :slim => true
        end

        jane = User.new :gender => :female
        jane.female? # => throws NoMethodError: undefined method `female?' 
        User.male    # => throws NoMethodError: undefined method `male' 
  
  Yet the setter and getter for <tt>gender</tt>, as well as the <tt>User.genders</tt> methods are still available, only all shortcut
  methods for each of the enumeration values are not generated.
  
  It's also possible to set <tt>:slim => :class</tt> which only disables the generation of any class-level shortcut method, because those
  are also available via the enhanced enumeration hash:
  
        class Message < ActiveRecord::Base
          as_enum :status, { :unread => 0, :read => 1, :archived => 99}, :slim => :class
        end
        
        msg = Message.new :body => 'Hello World!', status_cd => 0
        msg.read?                      # => false; shortuct methods on instance are still enabled
        msg.status                     # => :unread
        Message.unread                 # => throws NoMethodError: undefined method `unread`
        Message.statuses.unread        # => 0
        Message.statuses.unread(true)  # => :unread
        
* As a default an <tt>ArgumentError</tt> is raised if the user tries to set the field to an invalid enumeration value, to change this
  behaviour use the <tt>:whiny</tt> option:
  
        class User < ActiveRecord::Base
          as_enum :gender, [:male, :female], :whiny => false
        end
        
* To define any option globally, like setting <tt>:whiny</tt> to +false+, or globally enable <tt>:prefix</tt>; all default options
  are stored in <tt>SimpleEnum.default_options</tt>, this hash can be easily changed in your initializers or wherever:

        # e.g. setting :prefix => true (globally)
        SimpleEnum.default_options[:prefix] = true

== Best practices

Searching for certain values by using the finder methods:

    User.find :all, :conditions => { :gender_cd => User.female }
    
Working with database backed values, now assuming that there exists a +genders+ table:

    class Person < ActiveRecord::Base
      as_enum :gender, Gender.find(:all).map { |g| [g.name.to_sym, g.id] } # map to array of symbols
    end
    
Working with object backed values, the only requirement to enable this is that <em>a)</em> either a field name +name+ exists
or <em>b)</em> a custom method to convert an object to a symbolized form named +to_enum_sym+ (for general uses overriding
+to_enum+ is perfectly fine) exists:

    class Status < ActiveRecord::Base
      # this has a column named :name
      STATUSES = self.find(:all, :order => :name)
    end
    
    class BankTransaction < ActiveRecord::Base
      as_enum :status, Status.STATUSES
    end
    
    # what happens now? the id's of Status now serve as enumeration key and the
    # Status object as the value so...    
    t = BankTransaction.new
    t.pending!
    t.status # => #<Status id: 1, name: "pending">
        
    # and it's also possible to access the objects/values using:
    BankTransaction.statuses(:pending) # => 1, access by symbol (not) the object!
    BankTransaction.statuses.pending   # => 1
    BankTransaction.statuses.pending(true) # => #<Status id: 1, name: "pending">
    
== Known issues/Open items

* Maybe the <tt>:whiny</tt> option should default to <tt>false</tt>, so that generally no exceptions are thrown if a user fakes a request?
* Remove the <tt>values_for_...</tt> method sometimes, it's already deprecated anyway (in favor of <tt>Klass.<pluralized enum name></tt>)
* Clean-up code and tests -> bring down LOC ;) (but maintain Code LOC vs. Test LOC ratio which is currently 1:2.9)

== Licence & Copyright
Copyright (c) 2009 by Lukas Westermann, Licenced under MIT Licence (see LICENCE file)