meyouhealth/flexible_enum

View on GitHub
README.md

Summary

Maintainability
Test Coverage
[![Build Status](https://travis-ci.org/meyouhealth/flexible_enum.svg?branch=master)](https://travis-ci.org/meyouhealth/flexible_enum)
[![Code Climate](https://codeclimate.com/github/meyouhealth/flexible_enum/badges/gpa.svg)](https://codeclimate.com/github/meyouhealth/flexible_enum)

# 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.

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

Option values may be any type.

```ruby
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:

```ruby
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:

```ruby
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:

```ruby
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:

```ruby
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.

```ruby
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:

```ruby
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).

```ruby
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`:

```ruby
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.

```ruby
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`:

```ruby
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.

```ruby
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:

```ruby
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):

```ruby
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:

```ruby
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:

```ruby
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:

```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).

```ruby
User.unscope(where: :status).inactive
```

## Custom Options

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

```ruby
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:

```ruby
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:

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

## Enum Introspection

You may retrieve a list of all defined `flexible_enum`s on a particular class:

```ruby
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.

```ruby
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](https://github.com/meyouhealth/flexible_enum/blob/master/CONTRIBUTING.md).

## Releasing

* On master in a commit named `Version x.y.z` update [version.rb](lib/flexible_enum/version.rb) and [CHANGELOG.md](CHANGELOG.md) with the new version.

* Run `rake release` to build, tag the commit, push the tag, and release to Rubygems.

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

* Thank contributors via Twitter.


## About MeYou Health

![http://meyouhealth.com/](https://avatars3.githubusercontent.com/u/249181?v=3&s=200)

FlexibleEnum is maintained by MYH, Inc.