classy_enum 2.3.0 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -15,3 +15,4 @@ spec/reports
15
15
  test/tmp
16
16
  test/version_tmp
17
17
  tmp
18
+ .DS_Store
data/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # ClassyEnum Changelog
2
2
 
3
+ ## 3.0.0
4
+
5
+ * Removing ClassyEnum::Base.enum_classes in favor of using enum
6
+ inheritance to setup classes
7
+ * Removing ClassyEnum::Base.valid_options
8
+ * Removing ClassyEnum::Base.find
9
+ * Removing ClassyEnum::Base#name
10
+ * Removing :suffix option from classy_enum_attr
11
+ * Enforce use of namespacing for subclasses (Parent::Child < Parent)
12
+ * Use require instead of autoload
13
+ * Lots of code refactoring
14
+
3
15
  ## 2.3.0
4
16
 
5
17
  * Deprecating ClassyEnum::Base#name (use to_s.titleize instead). `name` is
data/README.md CHANGED
@@ -6,12 +6,12 @@ ClassyEnum is a Ruby on Rails gem that adds class-based enumerator functionality
6
6
 
7
7
  ## Rails & Ruby Versions Supported
8
8
 
9
- *Rails:*
9
+ *Rails:* 3.0.x - 3.2.x
10
10
 
11
- * 3.0.x - 3.2.x: Fully tested in a production application.
12
- * 2.3.x: If you need support for Rails 2.3.x, please install [version 0.9.1](https://rubygems.org/gems/classy_enum/versions/0.9.1)
11
+ *Ruby:* 1.8.7, 1.9.2 and 1.9.3
13
12
 
14
- *Ruby:* Ruby 1.8.7, 1.9.2, and 1.9.3 are tested and supported
13
+ If you need support for Rails 2.3.x, please install [version 0.9.1](https://rubygems.org/gems/classy_enum/versions/0.9.1).
14
+ Note: This branch is no longer maintained and will not get bug fixes or new features.
15
15
 
16
16
  ## Installation
17
17
 
@@ -19,15 +19,9 @@ The gem is hosted at [rubygems.org](https://rubygems.org/gems/classy_enum)
19
19
 
20
20
  You will also need to add `app/enums` as an autoloadable path. This configuration will depend on which version of rails you are using.
21
21
 
22
- ## Upgrading to 2.0
22
+ ## Upgrading?
23
23
 
24
- Prior to 2.0, enum classes were implicity defined and were only required
25
- when overriding methods or properties. As of 2.0, all enum classes must
26
- explicity subclass a child of ClassyEnum::Base. If you used the
27
- generator, there are no changes to the existing structure.
28
-
29
- Built-in Formtastic support has been removed. See the note at the
30
- bottom of this readme for more information how how to enable it.
24
+ See the [wiki](https://github.com/beerlington/classy_enum/wiki/Upgrading) for notes about upgrading from previous versions.
31
25
 
32
26
  ## Example Usage
33
27
 
@@ -45,20 +39,19 @@ A new enum template file will be created at app/enums/priority.rb that will look
45
39
 
46
40
  ```ruby
47
41
  class Priority < ClassyEnum::Base
48
- enum_classes :low, :medium, :high
49
42
  end
50
43
 
51
- class PriorityLow < Priority
44
+ class Priority::Low < Priority
52
45
  end
53
46
 
54
- class PriorityMedium < Priority
47
+ class Priority::Medium < Priority
55
48
  end
56
49
 
57
- class PriorityHigh < Priority
50
+ class Priority::High < Priority
58
51
  end
59
52
  ```
60
53
 
61
- The `enum_classes` class macro will define the enum member order as well as additional ClassyEnum behavior, which is described further down in this document.
54
+ The class order will define the enum member order as well as additional ClassyEnum behavior, which is described further down in this document.
62
55
 
63
56
  ### 2. Customize the Enum
64
57
 
@@ -70,20 +63,18 @@ I would like to add a method called `send_email?` that all member subclasses res
70
63
 
71
64
  ```ruby
72
65
  class Priority < ClassyEnum::Base
73
- enum_classes :low, :medium, :high
74
-
75
66
  def send_email?
76
67
  false
77
68
  end
78
69
  end
79
70
 
80
- class PriorityLow < Priority
71
+ class Priority::Low < Priority
81
72
  end
82
73
 
83
- class PriorityMedium < Priority
74
+ class Priority::Medium < Priority
84
75
  end
85
76
 
86
- class PriorityHigh < Priority
77
+ class Priority::High < Priority
87
78
  def send_email?
88
79
  true
89
80
  end
@@ -119,11 +110,10 @@ With this setup, I can now do the following:
119
110
  ```ruby
120
111
  @alarm = Alarm.create(:priority => :medium)
121
112
 
122
- @alarm.priority # => PriorityMedium
113
+ @alarm.priority # => Priority::Medium
123
114
  @alarm.priority.medium? # => true
124
115
  @alarm.priority.high? # => false
125
116
  @alarm.priority.to_s # => 'medium'
126
- @alarm.priority.name # => 'Medium'
127
117
 
128
118
  # Should this alarm send an email?
129
119
  @alarm.send_email? # => false
@@ -137,7 +127,7 @@ The enum field works like any other model attribute. It can be mass-assigned usi
137
127
 
138
128
  In some cases you may want an enum class to reference the owning object
139
129
  (an instance of the active record model). Think of it as a `belongs_to`
140
- relationship, where the enum can reference its owning object.
130
+ relationship, where the enum belongs to the model.
141
131
 
142
132
  By default, the back reference can be called using `owner`.
143
133
  If you want to refer to the owner by a different name, you must explicitly declare
@@ -147,14 +137,11 @@ Example using the default `owner` method:
147
137
 
148
138
  ```ruby
149
139
  class Priority < ClassyEnum::Base
150
- enum_classes :low, :medium, :high
151
140
  end
152
141
 
153
- ...
154
142
  # low and medium subclasses omitted
155
- ...
156
143
 
157
- class PriorityHigh < Priority
144
+ class Priority::High < Priority
158
145
  def send_email?
159
146
  owner.enabled?
160
147
  end
@@ -165,15 +152,12 @@ Example where the owner reference is explicitly declared:
165
152
 
166
153
  ```ruby
167
154
  class Priority < ClassyEnum::Base
168
- enum_classes :low, :medium, :high
169
155
  owner :alarm
170
156
  end
171
157
 
172
- ...
173
158
  # low and medium subclasses omitted
174
- ...
175
159
 
176
- class PriorityHigh < Priority
160
+ class Priority::High < Priority
177
161
  def send_email?
178
162
  alarm.enabled?
179
163
  end
@@ -213,28 +197,15 @@ end
213
197
 
214
198
  ## Special Cases
215
199
 
216
- What if your enum class name is not the same as your model's attribute name? No problem! Just use a second arugment in `classy_enum_attr` to declare the attribute name. In this case, the model's attribute is called *alarm_priority*.
200
+ What if your enum class name is not the same as your model's attribute name? No problem! Just use a second argument in `classy_enum_attr` to declare the attribute name. In this case, the model's attribute is called *alarm_priority*.
217
201
 
218
202
  ```ruby
219
203
  class Alarm < ActiveRecord::Base
220
- classy_enum_attr :alarm_priority, :enum => :priority
204
+ classy_enum_attr :alarm_priority, :enum => 'Priority'
221
205
  end
222
206
 
223
207
  @alarm = Alarm.create(:alarm_priority => :medium)
224
- @alarm.alarm_priority # => PriorityMedium
225
- ```
226
-
227
- If you would like the default getter method to return a string, you can
228
- use the optional *:suffix* option for the enum getter:
229
-
230
- ```ruby
231
- class Alarm < ActiveRecord::Base
232
- classy_enum_attr :priority, :suffix => 'type'
233
- end
234
-
235
- alarm = Alarm.create(:priority => :high)
236
- alarm.priority # => 'high'
237
- alarm.priority_type # instance of PriorityHigh enum
208
+ @alarm.alarm_priority # => Priority::Medium
238
209
  ```
239
210
 
240
211
  ## Model Validation
@@ -265,20 +236,13 @@ end
265
236
 
266
237
  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.
267
238
 
268
- Instantiate an enum member subclass *PriorityLow*
239
+ Instantiate an enum member subclass *Priority::Low*
269
240
 
270
241
  ```ruby
271
242
  # These statements are all equivalent
272
243
  low = Priority.build(:low)
273
244
  low = Priority.build('low')
274
- low = Priority.find(:low)
275
- low = PriorityLow.new
276
- ```
277
-
278
- Get a list of the valid enum options
279
-
280
- ```ruby
281
- Priority.valid_options # => low, medium, high
245
+ low = Priority::Low.new
282
246
  ```
283
247
 
284
248
  ## Formtastic Support
data/classy_enum.gemspec CHANGED
@@ -16,7 +16,7 @@ Gem::Specification.new do |gem|
16
16
 
17
17
  gem.add_dependency('rails', '>= 3.0')
18
18
 
19
- gem.add_development_dependency('rspec-rails', '~> 2.10.0')
19
+ gem.add_development_dependency('rspec-rails', '~> 2.11.0')
20
20
  gem.add_development_dependency('sqlite3', '~> 1.3.6')
21
21
  gem.add_development_dependency('json', '~> 1.6.5')
22
22
 
@@ -1,8 +1,6 @@
1
1
  source :rubygems
2
2
 
3
3
  gem 'rails', '~> 3.0.0'
4
- gem 'rspec-rails', '~> 2.8.1'
5
- gem 'formtastic', '~> 1.2.4'
4
+ gem 'rspec-rails', '~> 2.11.0'
6
5
  gem 'sqlite3-ruby', :require => 'sqlite3'
7
- gem 'json', '~> 1.6.5'
8
- gem "jeweler", "~> 1.6.2"
6
+ gem 'json', '~> 1.6.5'
@@ -1,8 +1,6 @@
1
1
  source :rubygems
2
2
 
3
3
  gem 'rails', '~> 3.1.0'
4
- gem 'rspec-rails', '~> 2.8.1'
5
- gem 'formtastic', '~> 1.2.4'
4
+ gem 'rspec-rails', '~> 2.11.0'
6
5
  gem 'sqlite3'
7
- gem 'json', '~> 1.6.5'
8
- gem "jeweler", "~> 1.6.2"
6
+ gem 'json', '~> 1.6.5'
@@ -1,8 +1,6 @@
1
1
  source :rubygems
2
2
 
3
3
  gem 'rails', '~> 3.2.0'
4
- gem 'rspec-rails', '~> 2.8.1'
5
- gem 'formtastic', '~> 1.2.4'
4
+ gem 'rspec-rails', '~> 2.11.0'
6
5
  gem 'sqlite3'
7
- gem 'json', '~> 1.6.5'
8
- gem "jeweler", "~> 1.6.2"
6
+ gem 'json', '~> 1.6.5'
@@ -0,0 +1,58 @@
1
+ module ClassyEnum
2
+ module ActiveRecord
3
+
4
+ # Class macro used to associate an enum with an attribute on an ActiveRecord model.
5
+ # This method is automatically added to all ActiveRecord models when the classy_enum gem
6
+ # is installed. Accepts an argument for the enum class to be associated with
7
+ # the model. ActiveRecord validation is automatically added to ensure
8
+ # that a value is one of its pre-defined enum members.
9
+ #
10
+ # ==== Example
11
+ # # Associate an enum Priority with Alarm model's priority attribute
12
+ # class Alarm < ActiveRecord::Base
13
+ # classy_enum_attr :priority
14
+ # end
15
+ #
16
+ # # Associate an enum Priority with Alarm model's alarm_priority attribute
17
+ # classy_enum_attr :alarm_priority, :enum => 'Priority'
18
+ #
19
+ # # Allow enum value to be nil
20
+ # classy_enum_attr :priority, :allow_nil => true
21
+ #
22
+ # # Allow enum value to be blank
23
+ # classy_enum_attr :priority, :allow_blank => true
24
+ def classy_enum_attr(attribute, options={})
25
+ enum = (options[:enum] || attribute).to_s.camelize.constantize
26
+ allow_blank = options[:allow_blank] || false
27
+ allow_nil = options[:allow_nil] || false
28
+ serialize_as_json = options[:serialize_as_json] || false
29
+
30
+ error_message = "must be #{enum.all.to_sentence(:two_words_connector => ' or ', :last_word_connector => ', or ')}"
31
+
32
+ # Add ActiveRecord validation to ensure it won't be saved unless it's an option
33
+ validates_inclusion_of attribute,
34
+ :in => enum.all,
35
+ :message => error_message,
36
+ :allow_blank => allow_blank,
37
+ :allow_nil => allow_nil
38
+
39
+ # Define getter method that returns a ClassyEnum instance
40
+ define_method attribute do
41
+ enum.build(read_attribute(attribute),
42
+ :owner => self,
43
+ :serialize_as_json => serialize_as_json,
44
+ :allow_blank => (allow_blank || allow_nil)
45
+ )
46
+ end
47
+
48
+ # Define setter method that accepts either string or symbol for member
49
+ define_method "#{attribute}=" do |value|
50
+ value = value.to_s unless value.nil?
51
+ super(value)
52
+ end
53
+ end
54
+
55
+ end
56
+ end
57
+
58
+ ActiveRecord::Base.send :extend, ClassyEnum::ActiveRecord
@@ -1,8 +1,88 @@
1
1
  module ClassyEnum
2
+ class SubclassNameError < Exception; end
3
+
2
4
  class Base
3
- extend ClassyEnum::ClassMethods
4
- include ClassyEnum::InstanceMethods
5
5
  include Comparable
6
- include ActiveModel::AttributeMethods
6
+ include Conversion
7
+ include Predicate
8
+ include Collection
9
+
10
+ class_attribute :base_class
11
+ attr_accessor :owner, :serialize_as_json, :allow_blank
12
+
13
+ class << self
14
+ def inherited(klass)
15
+ if self == ClassyEnum::Base
16
+ klass.base_class = klass
17
+ else
18
+
19
+ # Ensure subclasses follow expected naming conventions
20
+ unless klass.name.start_with? "#{base_class.name}::"
21
+ raise SubclassNameError, "subclass must be namespaced with #{base_class.name}::"
22
+ end
23
+
24
+ # Add visit_EnumMember methods to support validates_uniqueness_of with enum field
25
+ # This is due to a bug in Rails where it uses the method result as opposed to the
26
+ # database value for validation scopes. A fix will be released in Rails 4, but
27
+ # this will remain until Rails 3.x is no longer prevalent.
28
+ Arel::Visitors::ToSql.class_eval do
29
+ define_method "visit_#{klass.name.split('::').join('_')}", lambda {|value| quote(value.to_s) }
30
+ end
31
+
32
+ # Convert from MyEnumClass::NumberTwo to :number_two
33
+ enum = klass.name.split('::').last.underscore.to_sym
34
+
35
+ Predicate.define_predicate_method(klass, enum)
36
+
37
+ klass.instance_variable_set('@option', enum)
38
+ end
39
+
40
+ super
41
+ end
42
+
43
+ # Used internally to build a new ClassyEnum child instance
44
+ # It is preferred that you use ChildClass.new instead
45
+ #
46
+ # ==== Example
47
+ # # Create an Enum with some elements
48
+ # class Priority < ClassyEnum::Base
49
+ # end
50
+ #
51
+ # class Priority::Low < Priority
52
+ # end
53
+ #
54
+ # Priority.build(:low) # => Priority::Low.new
55
+ # Priority.build(:invalid_option) # => :invalid_option
56
+ def build(value, options={})
57
+ return value if value.blank? && options[:allow_blank]
58
+
59
+ # Return the value if it is not a valid member
60
+ return value unless all.map(&:to_s).include? value.to_s
61
+
62
+ object = "#{base_class}::#{value.to_s.camelize}".constantize.new
63
+ object.owner = options[:owner]
64
+ object.serialize_as_json = options[:serialize_as_json]
65
+ object.allow_blank = options[:allow_blank]
66
+ object
67
+ end
68
+
69
+ # DSL setter method for overriding reference to enum owner (ActiveRecord model)
70
+ #
71
+ # ==== Example
72
+ # # Create an Enum with some elements
73
+ # class Priority < ClassyEnum::Base
74
+ # owner :alarm
75
+ # end
76
+ #
77
+ # class Priority::High < Priority
78
+ # def send_alarm?
79
+ # alarm.enabled?
80
+ # end
81
+ # end
82
+ def owner(owner)
83
+ define_method owner, lambda { @owner }
84
+ end
85
+ end
86
+
7
87
  end
8
88
  end
@@ -0,0 +1,76 @@
1
+ module ClassyEnum
2
+ module Collection
3
+ # Sort an array of elements based on the order they are defined
4
+ #
5
+ # ==== Example
6
+ # # Create an Enum with some elements
7
+ # class Priority < ClassyEnum::Base
8
+ # end
9
+ #
10
+ # class Priority::Low < Priority; end
11
+ # class Priority::Medium < Priority; end
12
+ # class Priority::High < Priority; end
13
+ #
14
+ # @low = Priority.build(:low)
15
+ # @medium = Priority.build(:medium)
16
+ # @high = Priority.build(:high)
17
+ # priorities = [@low, @high, @medium]
18
+ # priorities.sort # => [@low, @medium, @high]
19
+ # priorities.max # => @high
20
+ # priorities.min # => @low
21
+ def <=> other
22
+ index <=> other.index
23
+ end
24
+
25
+ def self.included(klass)
26
+ klass.extend ClassMethods
27
+ end
28
+
29
+ module ClassMethods
30
+ def inherited(klass)
31
+ if self == ClassyEnum::Base
32
+ klass.class_attribute :enum_options
33
+ klass.enum_options = []
34
+ else
35
+ enum_options << klass
36
+ klass.instance_variable_set('@index', enum_options.size)
37
+ end
38
+
39
+ super
40
+ end
41
+
42
+ # Returns an array of all instantiated enums
43
+ #
44
+ # ==== Example
45
+ # # Create an Enum with some elements
46
+ # class Priority < ClassyEnum::Base
47
+ # end
48
+ #
49
+ # class Priority::Low < Priority; end
50
+ # class Priority::Medium < Priority; end
51
+ # class Priority::High < Priority; end
52
+ #
53
+ # Priority.all # => [Priority::Low.new, Priority::Medium.new, Priority::High.new]
54
+ def all
55
+ enum_options.map(&:new)
56
+ end
57
+
58
+ # Returns a 2D array for Rails select helper options.
59
+ # Also used internally for Formtastic support
60
+ #
61
+ # ==== Example
62
+ # # Create an Enum with some elements
63
+ # class Priority < ClassyEnum::Base
64
+ # end
65
+ #
66
+ # class Priority::Low < Priority; end
67
+ # class Priority::ReallyHigh < Priority; end
68
+ #
69
+ # Priority.select_options # => [["Low", "low"], ["Really High", "really_high"]]
70
+ def select_options
71
+ all.map {|e| [e.to_s.titleize, e.to_s] }
72
+ end
73
+ end
74
+
75
+ end
76
+ end
@@ -0,0 +1,69 @@
1
+ module ClassyEnum
2
+ module Conversion
3
+
4
+ # Returns an integer representing the order that this element was defined in.
5
+ # Also used internally for sorting.
6
+ #
7
+ # ==== Example
8
+ # # Create an Enum with some elements
9
+ # class Priority < ClassyEnum::Base
10
+ # end
11
+ #
12
+ # class Priority::Low < Priority; end
13
+ # class Priority::Medium < Priority; end
14
+ # class Priority::High < Priority; end
15
+ #
16
+ # @priority = Priority::Medium.new
17
+ # @priority.to_i # => 2
18
+ def to_i
19
+ self.class.instance_variable_get('@index')
20
+ end
21
+
22
+ alias :index :to_i
23
+
24
+ # Returns the name or string corresponding to element
25
+ #
26
+ # ==== Example
27
+ # # Create an Enum with some elements
28
+ # class Priority < ClassyEnum::Base
29
+ # end
30
+ #
31
+ # class Priority::Low < Priority; end
32
+ # class Priority::Medium < Priority; end
33
+ # class Priority::High < Priority; end
34
+ #
35
+ # @priority = Priority::Low.new
36
+ # @priority.to_s # => 'low'
37
+ def to_s
38
+ self.class.instance_variable_get('@option').to_s
39
+ end
40
+
41
+ # Returns a Symbol corresponding to a string representation of element,
42
+ # creating the symbol if it did not previously exist
43
+ #
44
+ # ==== Example
45
+ # # Create an Enum with some elements
46
+ # class Priority < ClassyEnum::Base
47
+ # end
48
+ #
49
+ # class Priority::Low < Priority; end
50
+ # class Priority::Medium < Priority; end
51
+ # class Priority::High < Priority; end
52
+ #
53
+ # @priority = Priority::Low.new
54
+ # @priority.to_sym # => :low
55
+ def to_sym
56
+ to_s.to_sym
57
+ end
58
+
59
+ # Overrides as_json to remove owner reference recursion issues
60
+ def as_json(options=nil)
61
+ return to_s unless serialize_as_json
62
+ json = super(options)
63
+ json.delete('owner')
64
+ json.delete('serialize_as_json')
65
+ json
66
+ end
67
+
68
+ end
69
+ end
@@ -0,0 +1,35 @@
1
+ module ClassyEnum
2
+ module Predicate
3
+
4
+ # Define attribute methods like two?
5
+ def self.define_predicate_method(klass, enum)
6
+ klass.base_class.class_eval do
7
+ define_method "#{enum}?", lambda { attribute?(enum.to_s) }
8
+ end
9
+ end
10
+
11
+ protected
12
+
13
+ # Determine if the enum attribute is a particular member.
14
+ #
15
+ # ==== Example
16
+ # # Create an Enum with some elements
17
+ # class Breed < ClassyEnum::Base
18
+ # end
19
+ #
20
+ # class Breed::GoldenRetriever < Breed; end
21
+ # class Breed::Snoop < Breed; end
22
+ #
23
+ # # Create an ActiveRecord class using the Breed enum
24
+ # class Dog < ActiveRecord::Base
25
+ # classy_enum_attr :breed
26
+ # end
27
+ #
28
+ # @dog = Dog.new(:breed => :snoop)
29
+ # @dog.breed.snoop? # => true
30
+ # @dog.breed.golden_retriever? # => false
31
+ def attribute?(attribute)
32
+ to_s == attribute
33
+ end
34
+ end
35
+ end
@@ -1,3 +1,3 @@
1
1
  module ClassyEnum
2
- VERSION = "2.3.0"
2
+ VERSION = "3.0.0"
3
3
  end
data/lib/classy_enum.rb CHANGED
@@ -1,8 +1,5 @@
1
- module ClassyEnum
2
- autoload :Base, 'classy_enum/base'
3
- autoload :InstanceMethods, 'classy_enum/instance_methods'
4
- autoload :ClassMethods, 'classy_enum/class_methods'
5
- autoload :Attributes, 'classy_enum/attributes'
6
- end
7
-
8
- ActiveRecord::Base.send :extend, ClassyEnum::Attributes
1
+ require 'classy_enum/collection'
2
+ require 'classy_enum/conversion'
3
+ require 'classy_enum/predicate'
4
+ require 'classy_enum/base'
5
+ require 'classy_enum/active_record'
@@ -1,5 +1,4 @@
1
1
  class <%= class_name %> < ClassyEnum::Base
2
- enum_classes <%= values.map {|a| ":#{a}"}.join(", ") %>
3
2
  end
4
3
  <% values.each do |arg| %>
5
4
  class <%= "#{class_name}::#{arg.camelize}" %> < <%= class_name %>