Skip to content
Browse files

Merge branch 'release-3.1'

  • Loading branch information...
2 parents f928d39 + 59f63df commit db53df65176c3920caeee7b12104a1e3322c6312 @beerlington committed Aug 12, 2012
View
16 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
View
87 README.md
@@ -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
@@ -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.
@@ -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
@@ -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
@@ -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
@@ -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:
@@ -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
```
View
1 lib/classy_enum.rb
@@ -1,3 +1,4 @@
+require 'classy_enum/translation'
require 'classy_enum/collection'
require 'classy_enum/conversion'
require 'classy_enum/predicate'
View
5 lib/classy_enum/active_record.rb
@@ -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
View
7 lib/classy_enum/base.rb
@@ -5,6 +5,7 @@ class Base
include Comparable
include Conversion
include Predicate
+ include Translation
include Collection
class_attribute :base_class
@@ -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]
View
48 lib/classy_enum/collection.rb
@@ -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
@@ -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
@@ -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
@@ -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
#
@@ -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
View
4 lib/classy_enum/predicate.rb
@@ -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
@@ -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
View
38 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
View
2 lib/classy_enum/version.rb
@@ -1,3 +1,3 @@
module ClassyEnum
- VERSION = "3.0.1"
+ VERSION = "3.1.0"
end
View
10 spec/classy_enum/active_record_spec.rb
@@ -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
View
49 spec/classy_enum/collection_spec.rb
@@ -1,6 +1,7 @@
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
class ClassyEnumCollection < ClassyEnum::Base
+ delegate :odd?, :to => :to_i
end
class ClassyEnumCollection::One < ClassyEnumCollection
@@ -13,11 +14,57 @@ class ClassyEnumCollection::Three < ClassyEnumCollection
end
describe ClassyEnum::Collection do
- subject { ClassyEnumCollection }
+ subject(:enum) { ClassyEnumCollection }
its(:enum_options) { should == [ClassyEnumCollection::One, ClassyEnumCollection::Two, ClassyEnumCollection::Three] }
its(:all) { should == [ClassyEnumCollection::One.new, ClassyEnumCollection::Two.new, ClassyEnumCollection::Three.new] }
its(:select_options) { should == [['One', 'one'],['Two', 'two'], ['Three', 'three']] }
+
+ context '.map' do
+ it 'should behave like an enumerable' do
+ enum.map(&:to_s).should == %w(one two three)
+ end
+ end
+
+ context '#<=> (equality)' do
+ its(:first) { should == ClassyEnumCollection::One.new }
+ its(:first) { should == :one }
+ its(:first) { should == 'one' }
+ its(:first) { should_not == :two }
+ its(:first) { should_not == :not_found }
+
+ its(:max) { should == :three }
+ end
+
+ context '.find, .detect, []' do
+ let(:expected_enum) { ClassyEnumCollection::Two.new }
+
+ [:find, :detect, :[]].each do |method|
+ it 'should return an instance when searching by symbol' do
+ enum.send(method, :two).should == expected_enum
+ end
+
+ it 'should return an instance when searching by string' do
+ enum.send(method, 'two').should == expected_enum
+ end
+
+ it 'should behave like an enumerable when using a block' do
+ enum.send(method) {|e| e.to_s == 'two'}.should == expected_enum
+ end
+
+ it 'should return nil if item cannot be found' do
+ enum.send(method, :not_found).should be_nil
+ end
+ end
+ end
+
+ context '.select' do
+ let(:expected_enum) { ClassyEnumCollection::Two.new }
+
+ it 'returns an array with each item where the block returns true' do
+ enum.select(&:odd?).should == [ClassyEnumCollection::One.new, ClassyEnumCollection::Three.new]
+ end
+ end
end
describe ClassyEnum::Collection, Comparable do
View
57 spec/classy_enum/translation_spec.rb
@@ -0,0 +1,57 @@
+require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
+
+class ClassyEnumTranslation < ClassyEnum::Base
+end
+
+class ClassyEnumTranslation::One < ClassyEnumTranslation
+end
+
+class ClassyEnumTranslation::Two < ClassyEnumTranslation
+end
+
+describe ClassyEnum::Translation do
+
+ before do
+ I18n.reload!
+ I18n.backend.store_translations :en, :classy_enum => {:classy_enum_translation => {:one => 'One!', :two => 'Two!' } }
+ I18n.backend.store_translations :es, :classy_enum => {:classy_enum_translation => {:one => 'Uno', :two => 'Dos' } }
+ end
+
+ context '#text' do
+ subject { ClassyEnumTranslation::One.new }
+
+ context 'default' do
+ before { I18n.reload! }
+ its(:text) { should == 'One' }
+ end
+
+ context 'en' do
+ before { I18n.locale = :en }
+ its(:text) { should == 'One!' }
+ end
+
+ context 'es' do
+ before { I18n.locale = :es }
+ its(:text) { should == 'Uno' }
+ end
+ end
+
+ context '.select_options' do
+ subject { ClassyEnumTranslation }
+
+ context 'default' do
+ before { I18n.reload! }
+ its(:select_options) { should == [["One", "one"], ["Two", "two"]] }
+ end
+
+ context 'en' do
+ before { I18n.locale = :en }
+ its(:select_options) { should == [["One!", "one"], ["Two!", "two"]] }
+ end
+
+ context 'es' do
+ before { I18n.locale = :es }
+ its(:select_options) { should == [["Uno", "one"], ["Dos", "two"]] }
+ end
+ end
+end

0 comments on commit db53df6

Please sign in to comment.
Something went wrong with that request. Please try again.