Skip to content

Commit

Permalink
Merge branch 'release-3.1'
Browse files Browse the repository at this point in the history
  • Loading branch information
beerlington committed Aug 12, 2012
2 parents f928d39 + 59f63df commit db53df6
Show file tree
Hide file tree
Showing 12 changed files with 292 additions and 32 deletions.
16 changes: 16 additions & 0 deletions CHANGELOG.md
@@ -1,5 +1,21 @@
# ClassyEnum Changelog

## 3.1.0

* ClassyEnum::Base now extends Enumerable to provide enum collection
methods. All objects in the collection are instances of the enum
members. .find is overridden to provide custom find functionality.
* ClassyEnum::Base.find has been reintroduced, with aliases of .detect
and [].
* Introducing I18n support and providing a ClassyEnum::Base#text method
that will automatically translate text values.
* Translation support was added to ClassyEnum::Base.select_options.
* Equality can now be determined using strings and symbols. The
following will return true:

Priority::Low.new == :low # => true
Priority::Low.new == 'low' # => true

## 3.0.0

* Removing ClassyEnum::Base.enum_classes in favor of using enum
Expand Down
87 changes: 76 additions & 11 deletions README.md
Expand Up @@ -4,6 +4,18 @@

ClassyEnum is a Ruby on Rails gem that adds class-based enumerator functionality to ActiveRecord attributes.

## README Topics

* [Example Usage](https://github.com/beerlington/classy_enum#example-usage)
* [Internationalization](https://github.com/beerlington/classy_enum#internationalization)
* [Using Enum as a Collection](https://github.com/beerlington/classy_enum#using-enum-as-a-collection)
* [Reference to Owning Object](https://github.com/beerlington/classy_enum#back-reference-to-owning-object)
* [Serializing as JSON](https://github.com/beerlington/classy_enum#serializing-as-json)
* [Special Cases](https://github.com/beerlington/classy_enum#special-cases)
* [Built-in Model Validation](https://github.com/beerlington/classy_enum#model-validation)
* [Using Enums Outside of ActiveRecord](https://github.com/beerlington/classy_enum#working-with-classyenum-outside-of-activerecord)
* [Formtastic Support](https://github.com/beerlington/classy_enum#formtastic-support)

## Rails & Ruby Versions Supported

*Rails:* 3.0.x - 3.2.x
Expand All @@ -17,8 +29,6 @@ Note: This branch is no longer maintained and will not get bug fixes or new feat

The gem is hosted at [rubygems.org](https://rubygems.org/gems/classy_enum)

You will also need to add `app/enums` as an autoloadable path. This configuration will depend on which version of rails you are using.

## Upgrading?

See the [wiki](https://github.com/beerlington/classy_enum/wiki/Upgrading) for notes about upgrading from previous versions.
Expand Down Expand Up @@ -59,7 +69,7 @@ The generator creates a default setup, but each enum member can be changed to fi

I have defined three priority levels: low, medium, and high. Each priority level can have different properties and methods associated with it.

I would like to add a method called `send_email?` that all member subclasses respond to. By default this method will return false, but will be overridden for high priority alarms to return true.
I would like to add a method called `#send_email?` that all member subclasses respond to. By default this method will return false, but will be overridden for high priority alarms to return true.

```ruby
class Priority < ClassyEnum::Base
Expand Down Expand Up @@ -95,7 +105,7 @@ end
Note: Alternatively, you may use an enum type if your database supports it. See
[this issue](https://github.com/beerlington/classy_enum/issues/12) for more information.

Then in my model I've added a line that calls `classy_enum_attr` with a single argument representing the enum I want to associate with my model. I am also delegating the send_email? method to my Priority enum class.
Then in my model I've added a line that calls `classy_enum_attr` with a single argument representing the enum I want to associate with my model. I am also delegating the `#send_email?` method to my Priority enum class.

```ruby
class Alarm < ActiveRecord::Base
Expand All @@ -121,19 +131,74 @@ With this setup, I can now do the following:
@alarm.send_email? # => true
```

The enum field works like any other model attribute. It can be mass-assigned using `update_attribute(s)`.
The enum field works like any other model attribute. It can be mass-assigned using `#update_attributes`.

## Internationalization

ClassyEnum provides built-in support for translations using Ruby's I18n
library. The translated values are provided via a `#text` method on each
enum object. Translations are automatically applied when a key is found
at `locale.classy_enum.enum_parent_class.enum_value`, or a default value
is used that is equivalent to `#to_s.titleize`.

Given the following file *config/locales/es.yml*

```yml
es:
classy_enum:
priority:
low: 'Bajo'
medium: 'Medio'
high: 'Alto'
```

You can now do the following:

```ruby
@alarm.priority = :low
@alarm.priority.text # => 'Low'

I18n.locale = :es

@alarm.priority.text # => 'Bajo'
```

## Using Enum as a Collection

ClassyEnum::Base extends the [Enumerable module](http://ruby-doc.org/core-1.9.3/Enumerable.html)
which provides several traversal and searching methods. This can
be useful for situations where you are working with the collection,
as opposed to the attributes on an ActiveRecord object.

```ruby
# Find the priority based on string or symbol:
Priority.find(:low) # => Priority::Low.new
Priority.find('medium') # => Priority::Medium.new

# Find the lowest priority that can send email:
Priority.find(&:send_email?) # => Priority::High.new

# Find the priorities that are lower than Priority::High
high_priority = Priority::High.new
Priority.select {|p| p < high_priority } # => [Priority::Low.new, Priority::Medium.new]

# Iterate over each priority:
Priority.each do |priority|
puts priority.send_email?
end
```

## Back reference to owning object

In some cases you may want an enum class to reference the owning object
(an instance of the active record model). Think of it as a `belongs_to`
relationship, where the enum belongs to the model.

By default, the back reference can be called using `owner`.
By default, the back reference can be called using `#owner`.
If you want to refer to the owner by a different name, you must explicitly declare
the owner name in the classy_enum parent class using the `owner` class method.
the owner name in the classy_enum parent class using the `.owner` class method.

Example using the default `owner` method:
Example using the default `#owner` method:

```ruby
class Priority < ClassyEnum::Base
Expand Down Expand Up @@ -210,7 +275,7 @@ end

## Model Validation

An ActiveRecord validator `validates_inclusion_of :field, :in => ENUM.all` is automatically added to your model when you use `classy_enum_attr`.
An ActiveRecord validator `validates_inclusion_of :field, :in => ENUM` is automatically added to your model when you use `classy_enum_attr`.

If your enum only has members low, medium, and high, then the following validation behavior would be expected:

Expand Down Expand Up @@ -240,8 +305,8 @@ Instantiate an enum member subclass *Priority::Low*

```ruby
# These statements are all equivalent
low = Priority.build(:low)
low = Priority.build('low')
low = Priority.find(:low)
low = Priority.find('low')
low = Priority::Low.new
```

Expand Down
1 change: 1 addition & 0 deletions lib/classy_enum.rb
@@ -1,3 +1,4 @@
require 'classy_enum/translation'
require 'classy_enum/collection'
require 'classy_enum/conversion'
require 'classy_enum/predicate'
Expand Down
5 changes: 1 addition & 4 deletions lib/classy_enum/active_record.rb
Expand Up @@ -27,12 +27,9 @@ def classy_enum_attr(attribute, options={})
allow_nil = options[:allow_nil] || false
serialize_as_json = options[:serialize_as_json] || false

error_message = "must be #{enum.all.to_sentence(:two_words_connector => ' or ', :last_word_connector => ', or ')}"

# Add ActiveRecord validation to ensure it won't be saved unless it's an option
validates_inclusion_of attribute,
:in => enum.all,
:message => error_message,
:in => enum,
:allow_blank => allow_blank,
:allow_nil => allow_nil

Expand Down
7 changes: 3 additions & 4 deletions lib/classy_enum/base.rb
Expand Up @@ -5,6 +5,7 @@ class Base
include Comparable
include Conversion
include Predicate
include Translation
include Collection

class_attribute :base_class
Expand Down Expand Up @@ -56,12 +57,10 @@ def inherited(klass)
# Priority.build(:low) # => Priority::Low.new
# Priority.build(:invalid_option) # => :invalid_option
def build(value, options={})
return value if value.blank? && options[:allow_blank]
object = find(value)

# Return the value if it is not a valid member
return value unless all.map(&:to_s).include? value.to_s
return value if object.nil? || (options[:allow_blank] && object.nil?)

object = "#{base_class}::#{value.to_s.camelize}".constantize.new
object.owner = options[:owner]
object.serialize_as_json = options[:serialize_as_json]
object.allow_blank = options[:allow_blank]
Expand Down
48 changes: 43 additions & 5 deletions lib/classy_enum/collection.rb
Expand Up @@ -19,6 +19,10 @@ module Collection
# priorities.max # => @high
# priorities.min # => @low
def <=> other
if other.is_a?(Symbol) || other.is_a?(String)
other = self.class.find(other)
end

index <=> other.index
end

Expand All @@ -27,6 +31,9 @@ def self.included(klass)
end

module ClassMethods
include Enumerable
alias all to_a

def inherited(klass)
if self == ClassyEnum::Base
klass.class_attribute :enum_options
Expand All @@ -39,7 +46,7 @@ def inherited(klass)
super
end

# Returns an array of all instantiated enums
# Iterates over instances of each enum in the collection
#
# ==== Example
# # Create an Enum with some elements
Expand All @@ -50,11 +57,42 @@ def inherited(klass)
# class Priority::Medium < Priority; end
# class Priority::High < Priority; end
#
# Priority.all # => [Priority::Low.new, Priority::Medium.new, Priority::High.new]
def all
enum_options.map(&:new)
# Priority.each do |priority|
# puts priority # => 'Low', 'Medium', 'High'
# end
def each
enum_options.each {|e| yield e.new }
end

# Finds an enum instance by symbol, string, or block.
#
# If a block is given, it passes each entry in enum to block, and returns
# the first enum for which block is not false. If no enum matches, it
# returns nil.
#
# ==== Example
# # Create an Enum with some elements
# class Priority < ClassyEnum::Base
# end
#
# class Priority::Low < Priority; end
# class Priority::Medium < Priority; end
# class Priority::High < Priority; end
#
# Priority.find(:high) # => Priority::High.new
# Priority.find('high') # => Priority::High.new
# Priority.find {|e| e.to_sym == :high } # => Priority::High.new
def find(key=nil)
if block_given?
super
elsif map(&:to_s).include? key.to_s
super { |e| e.to_s == key.to_s }
end
end

alias detect find
alias [] find

# Returns a 2D array for Rails select helper options.
# Also used internally for Formtastic support
#
Expand All @@ -68,7 +106,7 @@ def all
#
# Priority.select_options # => [["Low", "low"], ["Really High", "really_high"]]
def select_options
all.map {|e| [e.to_s.titleize, e.to_s] }
map {|e| [e.text, e.to_s] }
end
end

Expand Down
4 changes: 2 additions & 2 deletions lib/classy_enum/predicate.rb
Expand Up @@ -4,7 +4,7 @@ module Predicate
# Define attribute methods like two?
def self.define_predicate_method(klass, enum)
klass.base_class.class_eval do
define_method "#{enum}?", lambda { attribute?(enum.to_s) }
define_method "#{enum}?", lambda { attribute?(enum) }
end
end

Expand All @@ -29,7 +29,7 @@ def self.define_predicate_method(klass, enum)
# @dog.breed.snoop? # => true
# @dog.breed.golden_retriever? # => false
def attribute?(attribute)
to_s == attribute
self == attribute
end
end
end
38 changes: 38 additions & 0 deletions lib/classy_enum/translation.rb
@@ -0,0 +1,38 @@
require 'i18n'

module ClassyEnum
module Translation

# Returns a translated string of the enum type. Used internally to create
# the select_options array.
#
# Translation location is:
# locale.classy_enum.base_class.enum_string
#
# ==== Example
# # Create an Enum with some elements
# class Priority < ClassyEnum::Base
# end
#
# class Priority::Low < Priority; end
# class Priority::ReallyHigh < Priority; end
#
# # Default translations are `to_s.titlieze`
# Priority::Low.new.text # => 'Low'
# Priority::ReallyHigh.new.text # => 'Really High'
#
# # Assuming we have a translation defined for:
# # es.classy_enum.priority.low # => 'Bajo'
#
# Priority::Low.new.text # => 'Bajo'
def text
I18n.translate to_s, :scope => i18n_scope, :default => to_s.titleize
end

private

def i18n_scope
[:classy_enum, base_class.name.underscore]
end
end
end
2 changes: 1 addition & 1 deletion lib/classy_enum/version.rb
@@ -1,3 +1,3 @@
module ClassyEnum
VERSION = "3.0.1"
VERSION = "3.1.0"
end
10 changes: 6 additions & 4 deletions spec/classy_enum/active_record_spec.rb
Expand Up @@ -43,10 +43,12 @@ class OtherDog < ActiveRecord::Base
specify { Dog.new(:breed => '').should_not be_valid }

context "with valid breed options" do
subject { Dog.new(:breed => :golden_retriever) }
it { should be_valid }
its(:breed) { should be_a(Breed::GoldenRetriever) }
its('breed.allow_blank') { should be_false }
[:golden_retriever, 'golden_retriever', Breed::GoldenRetriever.new].each do |option|
subject { Dog.new(:breed => option) }
it { should be_valid }
its(:breed) { should be_a(Breed::GoldenRetriever) }
its('breed.allow_blank') { should be_false }
end
end

context "with invalid breed options" do
Expand Down

0 comments on commit db53df6

Please sign in to comment.