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


* Removing ClassyEnum::Base.enum_classes in favor of using enum * 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. 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 & Ruby Versions Supported


*Rails:* 3.0.x - 3.2.x *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) 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? ## Upgrading?


See the [wiki](https://github.com/beerlington/classy_enum/wiki/Upgrading) for notes about upgrading from previous versions. 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 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 ```ruby
class Priority < ClassyEnum::Base 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 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. [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 ```ruby
class Alarm < ActiveRecord::Base class Alarm < ActiveRecord::Base
Expand All @@ -121,19 +131,74 @@ With this setup, I can now do the following:
@alarm.send_email? # => true @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 ## Back reference to owning object


In some cases you may want an enum class to reference the 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` (an instance of the active record model). Think of it as a `belongs_to`
relationship, where the enum belongs to the model. 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 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 ```ruby
class Priority < ClassyEnum::Base class Priority < ClassyEnum::Base
Expand Down Expand Up @@ -210,7 +275,7 @@ end


## Model Validation ## 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: 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 ```ruby
# These statements are all equivalent # These statements are all equivalent
low = Priority.build(:low) low = Priority.find(:low)
low = Priority.build('low') low = Priority.find('low')
low = Priority::Low.new 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/collection'
require 'classy_enum/conversion' require 'classy_enum/conversion'
require 'classy_enum/predicate' 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 allow_nil = options[:allow_nil] || false
serialize_as_json = options[:serialize_as_json] || 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 # Add ActiveRecord validation to ensure it won't be saved unless it's an option
validates_inclusion_of attribute, validates_inclusion_of attribute,
:in => enum.all, :in => enum,
:message => error_message,
:allow_blank => allow_blank, :allow_blank => allow_blank,
:allow_nil => allow_nil :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 Comparable
include Conversion include Conversion
include Predicate include Predicate
include Translation
include Collection include Collection


class_attribute :base_class class_attribute :base_class
Expand Down Expand Up @@ -56,12 +57,10 @@ def inherited(klass)
# Priority.build(:low) # => Priority::Low.new # Priority.build(:low) # => Priority::Low.new
# Priority.build(:invalid_option) # => :invalid_option # Priority.build(:invalid_option) # => :invalid_option
def build(value, options={}) 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 if object.nil? || (options[:allow_blank] && object.nil?)
return value unless all.map(&:to_s).include? value.to_s


object = "#{base_class}::#{value.to_s.camelize}".constantize.new
object.owner = options[:owner] object.owner = options[:owner]
object.serialize_as_json = options[:serialize_as_json] object.serialize_as_json = options[:serialize_as_json]
object.allow_blank = options[:allow_blank] 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.max # => @high
# priorities.min # => @low # priorities.min # => @low
def <=> other def <=> other
if other.is_a?(Symbol) || other.is_a?(String)
other = self.class.find(other)
end

index <=> other.index index <=> other.index
end end


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


module ClassMethods module ClassMethods
include Enumerable
alias all to_a

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


# Returns an array of all instantiated enums # Iterates over instances of each enum in the collection
# #
# ==== Example # ==== Example
# # Create an Enum with some elements # # Create an Enum with some elements
Expand All @@ -50,11 +57,42 @@ def inherited(klass)
# class Priority::Medium < Priority; end # class Priority::Medium < Priority; end
# class Priority::High < Priority; end # class Priority::High < Priority; end
# #
# Priority.all # => [Priority::Low.new, Priority::Medium.new, Priority::High.new] # Priority.each do |priority|
def all # puts priority # => 'Low', 'Medium', 'High'
enum_options.map(&:new) # end
def each
enum_options.each {|e| yield e.new }
end 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. # Returns a 2D array for Rails select helper options.
# Also used internally for Formtastic support # Also used internally for Formtastic support
# #
Expand All @@ -68,7 +106,7 @@ def all
# #
# Priority.select_options # => [["Low", "low"], ["Really High", "really_high"]] # Priority.select_options # => [["Low", "low"], ["Really High", "really_high"]]
def select_options def select_options
all.map {|e| [e.to_s.titleize, e.to_s] } map {|e| [e.text, e.to_s] }
end end
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? # Define attribute methods like two?
def self.define_predicate_method(klass, enum) def self.define_predicate_method(klass, enum)
klass.base_class.class_eval do klass.base_class.class_eval do
define_method "#{enum}?", lambda { attribute?(enum.to_s) } define_method "#{enum}?", lambda { attribute?(enum) }
end end
end end


Expand All @@ -29,7 +29,7 @@ def self.define_predicate_method(klass, enum)
# @dog.breed.snoop? # => true # @dog.breed.snoop? # => true
# @dog.breed.golden_retriever? # => false # @dog.breed.golden_retriever? # => false
def attribute?(attribute) def attribute?(attribute)
to_s == attribute self == attribute
end end
end 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 module ClassyEnum
VERSION = "3.0.1" VERSION = "3.1.0"
end 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 } specify { Dog.new(:breed => '').should_not be_valid }


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


context "with invalid breed options" do context "with invalid breed options" do
Expand Down

0 comments on commit db53df6

Please sign in to comment.