Skip to content
Browse files

merged

  • Loading branch information...
2 parents c280590 + 5d5b6c4 commit 9252ccdf998130d05d336aca67e01b2aaef64782 @kristianmandrup committed Sep 17, 2012
View
23 CHANGELOG.md
@@ -1,5 +1,28 @@
# ClassyEnum Changelog
+## 3.1.1
+
+* Fixes a regression with Formtastic support. ClassyEnumm::Base.build now
+ returns a null object that decends from the base_class when the argument is
+ blank (nil, empty string, etc). This allows the ActiveRecord model's enum
+ attribute to respond to enum methods even if it is blank.
+
+## 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
95 README.md
@@ -4,6 +4,17 @@
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)
+* [Formtastic Support](https://github.com/beerlington/classy_enum#formtastic-support)
+
## Rails & Ruby Versions Supported
*Rails:* 3.0.x - 3.2.x
@@ -17,8 +28,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.
@@ -91,7 +100,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
@@ -139,7 +148,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
@@ -165,19 +174,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
@@ -254,7 +318,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:
@@ -276,19 +340,6 @@ end
@alarm.valid? # => true
```
-## Working with ClassyEnum outside of ActiveRecord
-
-While ClassyEnum was designed to be used directly with ActiveRecord, it can also be used outside of it. Here are some examples based on the enum class defined earlier in this document.
-
-Instantiate an enum member subclass *Priority::Low*
-
-```ruby
-# These statements are all equivalent
-low = Priority.build(:low)
-low = Priority.build('low')
-low = Priority::Low.new
-```
-
## Formtastic Support
Built-in Formtastic support has been removed as of 2.0. It is still
View
3 lib/classy_enum.rb
@@ -1,8 +1,9 @@
+require 'classy_enum/translation'
require 'classy_enum/collection'
require 'classy_enum/conversion'
require 'classy_enum/predicate'
require 'classy_enum/valid_values'
require 'classy_enum/base'
require 'classy_enum/active_record'
-require 'classy_enum/railtie' if defined?(Rails) && defined?(Rails::Railtie)
+require 'classy_enum/railtie' if defined?(Rails) && defined?(Rails::Railtie)
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
17 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
@@ -20,6 +21,8 @@ def valid_values
end
def inherited(klass)
+ return if klass.anonymous?
+
if self == ClassyEnum::Base
klass.base_class = klass
klass.send :include, ClassyEnum::ValidValues
@@ -68,12 +71,18 @@ 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)
+
+ if object.nil? || (options[:allow_blank] && object.nil?)
+ return value unless value.blank?
- # Return the value if it is not a valid member
- return value unless all.map(&:to_s).include? value.to_s
+ # Subclass the base class and make it behave like the value that it is
+ object = Class.new(base_class) {
+ instance_variable_set(:@option, value)
+ delegate :blank?, :nil?, :to => :option
+ }.new
+ end
- 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
@@ -21,10 +21,17 @@ 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
module ClassMethods
+ include Enumerable
+ alias all to_a
+
def inherited(klass)
if self == ClassyEnum::Base
klass.class_attribute :enum_options
@@ -37,7 +44,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
@@ -48,11 +55,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
#
@@ -66,7 +104,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
8 lib/classy_enum/conversion.rb
@@ -35,7 +35,7 @@ def to_i
# @priority = Priority::Low.new
# @priority.to_s # => 'low'
def to_s
- self.class.instance_variable_get('@option').to_s
+ option.to_s
end
# Returns a Symbol corresponding to a string representation of element,
@@ -65,5 +65,11 @@ def as_json(options=nil)
json
end
+ private
+
+ def option
+ self.class.instance_variable_get(:@option)
+ end
+
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.1"
end
View
14 spec/classy_enum/active_record_spec.rb
@@ -43,14 +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 }
-
- its('breed.valid_values') { should == [:golden_retriever, :snoop, :husky] }
- specify { subject.breed.valid?(:snoop).should be_true }
- specify { subject.breed.valid?(:snoop_dog).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
14 spec/classy_enum/base_spec.rb
@@ -26,6 +26,20 @@ class ClassyEnumBase::Two < ClassyEnumBase
subject { ClassyEnumBase.build(:two) }
it { should be_a(ClassyEnumBase::Two) }
end
+
+ context 'nil' do
+ subject { ClassyEnumBase.build(nil) }
+ it { should be_a(ClassyEnumBase) }
+ it { should be_nil }
+ it { should be_blank }
+ end
+
+ context 'empty string' do
+ subject { ClassyEnumBase.build('') }
+ it { should be_a(ClassyEnumBase) }
+ it { should_not be_nil }
+ it { should be_blank }
+ end
end
context '#new' 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 9252ccd

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