Build Status Code Climate

FlexibleEnum

Give Ruby enum-like powers.

Installation

Add this line to your application's Gemfile:

gem "flexible_enum"

And then execute:

$ bundle

Or install it yourself as:

$ gem install flexible_enum

Basic Usage

The flexible_enum class method is mixed into ActiveRecord::Base. Call it to add enum-like powers to any number of existing attributes on a target class. You must provide the name of the attribute and a list of available options. Options consist of a name, value, and optional hash of configuration parameters.

class User < ActiveRecord::Base
  flexible_enum :status do
    active    0
    disabled  1
    pending   2
  end
end

Option values may be any type.

class Product < ActiveRecord::Base
  flexible_enum :manufacturer do
    honeywell "HON"
    sharp "SHCAY"
  end
end

Working with Values

Available options for each attribute are defined as constants on the target class. The classes above would have defined:

User::ACTIVE        # => 0
User::DISABLED      # => 1
User::PENDING       # => 2
Product::HONEYWELL  # => "HON"
Product::SHARP      # => "SHCAY"

Setter Methods

FlexibleEnum adds convenience methods for changing the current value of an attribute and immediately saving it to the database. By default, bang methods are added for each option:

u = User.new
u.active!   # Calls update_attributes(status: 0)
u.disabled! # Calls update_attributes(status: 1)

The name of the setter method can be changed using option configuration parameters:

class Post < ActiveRecord::Base
  flexible_enum :visibility do
    invisible 0, setter: :hide!
    visible   1, setter: :show!
  end
end

p = Post.new
p.show! # Calls update_attributes(visibility: 1)
p.hide! # Calls update_attributes(visibility: 0)

Timestamps

If the target class defines a date and/or time attribute corresponding to the flexible enum option being set it will be updated with the current date/time when using setter methods. For example, Post#show! above will set visibility = 1, visibile_at = Time.now.utc, and visible_on = Time.now.utc.to_date if those columns exist. The existance of columns is checked using ActiveRecord's attribute_method? method.

Use the :timestamp_attribute option configuration parameter to change the columns used:

flexible_enum :status do
  unknown  0
  active   1, timestamp_attribute: :actived
  disabled 2, timestamp_attribute: :disabled
end

Calling active! will now attempt to set actived_at and actived_on.

Predicate Methods

FlexibleEnum adds convenience methods for checking whether an option's value is also the attribute's current value.

p = Post.new
p.show!
p.visible?   # => true
p.invisible? # => false

Inverse predicate methods can be added by setting the :inverse configuration parameter. Inverse predicate methods have the reverse logic:

class Car < ActiveRecord::Base
  flexible_enum :fuel_type do
    gasoline 0
    diesel   1
    electric 2, inverse: :carbon_emitter
  end
end

c = Car.new
c.gasoline!
c.carbon_emitter? # => true
c.diesel!
c.carbon_emitter? # => true
c.electric!
c.carbon_emitter? # => false

Humanized Values

Humanized versions of attributes are available. This is convenient for displaying the current value on screen (see "Option Reflection" for rendering drop down lists).

c = Car.new(fuel_type: Car::DIESEL)
c.human_fuel_type = "Diesel"
Car.human_fuel_type(0) # => "Gasoline"
Car.fuel_types.collect(&:human_name) # => ["Gasoline", "Diesel", "Electric"]

If the flexible enum value is nil, the humanized name will also be nil:

c = Car.new(fuel_type: nil)
c.human_fuel_type # => nil

Name Method

The name of the attribute value is available. This allows you to grab the stringified version of the name of the value.

c = Car.new(fuel_type: Car::CARBON_EMITTER)
c.fuel_type_name # => "carbon_emitter"

If the flexible enum value is nil, the name will also be nil:

c = Car.new(fuel_type: nil)
c.fuel_type_name # => nil

Namespaced Attributes

FlexibleEnum attributes may be namespaced. Adding the namespace option to flexible_enum results in constants being defined in a new module.

class CashRegister < ActiveRecord::Base
  flexible_enum :drawer_position, namespace: "DrawerPositions" do
    opened 0
    closed 1
  end
end

# Constants are defined in a new module
CashRegister::DrawerPositions::OPENED # => 0
CashRegister::DrawerPositions::CLOSED # => 1

# Convenience methods are not affected by namespace
r = CashRegister.new
r.opened!
r.closed!

Scopes

FlexibleEnum adds ActiveRecord scopes for each attribute option:

User.active   # => User.where(status: 0)
User.disabled # => User.where(status: 1)
User.pending  # => User.where(status: 2)

When an attribute is namespaced a prefix is added to scope names. The prefix is the singularized namespace name (using Active Support):

CashRegister.drawer_position_opened # => CashRegister.where(drawer_position: 0)
CashRegister.drawer_position_closed # => CashRegister.where(drawer_position: 1)

Inverse scopes can be added by setting the :inverse configuration parameter:

class Car < ActiveRecord::Base
  flexible_enum :fuel_type do
    gasoline 0
    diesel   1
    electric 2, inverse: :carbon_emitter
  end
end

gas = Car.create(fuel_type: Car::GASOLINE)
diesel = Car.create(fuel_type: Car::DIESEL)
electric = Car.create(fuel_type: Car::ELECTRIC)

Car.carbon_emitter # => [gasoline, diesel]

Note about default scopes

Be careful when using default scopes on FlexibleEnum columns. Since FlexibleEnum provides scopes for enum values, setting a default_scope on a FlexibleEnum column will result in conflicts. For example, given this model:

class User < ActiveRecord::Base
  flexible_enum :status do
    active 1
    inactive 2
  end

  default_scope -> { where(status: ACTIVE) }
end

Attempts to use the User.inactive scope that FlexibleEnum provides will result in this SQL:

SELECT * FROM users WHERE users.status = 1 AND users.status = 2

You will need to unscope the default_scope before using a FlexibleEnum-provided scope (as you would have to do for normal Rails scopes that happen to contradict each other).

User.unscope(where: :status).inactive

Custom Options

Configuration parameters passed to attribute options are saved even if they are unknown.

class EmailEvent < ActiveRecord::Base
  flexible_enum :event_type do
    bounce    1, processor_class: RejectedProcessor
    dropped   2, processor_class: RejectedProcessor
    opened    3, processor_class: EmailOpenedProcessor
    delivered 4, processor_class: DeliveryProcessor
  end
end

Custom configuration parameters are available as an instance method on the object as well.

e = EmailEvent.new(event_type: 1)
e.event_type_details # => { processor_class: RejectedProcessor, value: 1 }

Option Introspection

You may introspect on available options and their configuration parameters:

ary = EmailEvent.event_types
ary.collect(&:name)       # => ["bounce", "dropped", "opened", "delivered"]
ary.collect(&:human_name) # => ["Bounce", "Dropped", "Opened", "Delivered"]
ary.collect(&:value)      # => [1, 2, 3, 4]

This works particularly well with ActionView:

f.collection_select(:event_type, EmailEvent.event_types, :value, :human_name)

Enum Introspection

You may retrieve a list of all defined flexible_enums on a particular class:

class Car < ActiveRecord::Base
  flexible_enum :status do
    new  1
    used 2
  end

  flexible_enum :car_type do
    gas      1
    hybrid   2
    electric 3
  end
end

Car.flexible_enums # => { status: Car.statuses, car_type: Car.car_types }

Overriding Methods

You may override any method defined on the target class by FlexibleEnum. In version 0.0.1, super behaved as it would without FlexibleEnum being present, you could not call a FlexibleEnum method implementation from an overriding method. As of version 0.0.2, super instead references the FlexibleEnum implementation of a method when overriding a FlexibleEnum-defined method.

class Item < ActiveRecord::Base
  flexible_enum :availability do
    discontinued 0
    backorder    1
    in_stock     2
  end

  # Version 0.0.1
  # Calling super would throw NoMethodError so we'd have to reimplement the method.
  def in_stock!
    BackInStockNotifier.new(self).queue if backorder?
    update_attribute!(status: IN_STOCK)
  end

  # Version 0.0.2
  # Calling super works and is preferred.
  def in_stock!
    BackInStockNotifier.new(self).queue if backorder?
    super
  end
end

Contributing

Please see CONTRIBUTING.md.

Releasing

  • On master in a commit named Version x.y.z update version.rb and CHANGELOG.md with the new version.

  • Run rake release to build and release to Rubygems.

  • Run git push origin master --tags to push master and the new tag (created automatically in previous step) to Github.

  • Create a Github release including the new change log entries in the description.

  • Thank contributors via Twitter.

About MeYou Health

http://meyouhealth.com/

FlexibleEnum is maintained by MYH, Inc.