flexible_enum 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: a885917e4b7e8bb397a0dab1d78711395ab49120
4
+ data.tar.gz: b0baf4ab1c1bd0cfc3c337e5cc585c71043ff96f
5
+ SHA512:
6
+ metadata.gz: cc8c6c0dc01306ccff5e961de08e66457486b4ef8f3e8ba5a716e356c8ad1b69a5403e51a158f8690e5d3b9a426bfea74c5807791a5dab83dd00d3fca1a7abc3
7
+ data.tar.gz: da33ab81de96f8813b6cde44c4bafee0ec5b0de44a8f87749c0a2661a2464ca80d626648f6fb9ef31f0af41901d33267b5aafbff7caeea359bc669769e0caf2f
data/.gitignore ADDED
@@ -0,0 +1,19 @@
1
+ *.gem
2
+ *.rbc
3
+ *.swp
4
+ .bundle
5
+ .config
6
+ .yardoc
7
+ Gemfile.lock
8
+ InstalledFiles
9
+ _yardoc
10
+ coverage
11
+ doc/
12
+ lib/bundler/man
13
+ pkg
14
+ rdoc
15
+ spec/reports
16
+ test/tmp
17
+ test/version_tmp
18
+ tmp
19
+ gemfiles/*.lock
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ ---
2
+ language: ruby
3
+ rvm: 2.1.2
4
+ before_script: bundle exec appraisal install
5
+ script: bundle exec appraisal rspec
data/Appraisals ADDED
@@ -0,0 +1,7 @@
1
+ appraise "rails-4.1" do
2
+ gem "rails", "~> 4.1.10"
3
+ end
4
+
5
+ appraise "rails-4.2" do
6
+ gem "rails", "~> 4.2.1"
7
+ end
data/CONTRIBUTING.md ADDED
@@ -0,0 +1,17 @@
1
+ # Contributing to FlexibleEnum
2
+
3
+ By participating in this project, you agree to abide by the MeYou Health [code of conduct].
4
+
5
+ [code of conduct]: http://engineering.meyouhealth.com/open-source/code-of-conduct
6
+
7
+ To contribute to FlexibleEnum:
8
+
9
+ 1. Fork it
10
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
11
+ 3. Run the tests (`appraisal rspec`)
12
+ 4. Commit your changes (`git commit -am 'Add some feature'`)
13
+ 5. Push to the branch (`git push origin my-new-feature`)
14
+ 6. Create new Pull Request
15
+
16
+ Want more detail on these steps? [Learn more about forking git
17
+ repositories](https://guides.github.com/activities/forking/).
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'http://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in flexible_enum.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013–2015 MeYou Health
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,295 @@
1
+ # FlexibleEnum
2
+
3
+ Give Ruby enum-like powers.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem "flexible_enum", git: 'git@github.com:meyouhealth/flexible_enum.git'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install flexible_enum
18
+
19
+ ## Basic Usage
20
+
21
+ The `flexible_enum` class method is mixed into ActiveRecord::Base. Call it to add enum-like powers to any number of existing attributes on a target class.
22
+ You must provide the name of the attribute and a list of available options. Options consist of a name, value, and optional hash of configuration parameters.
23
+
24
+ ```ruby
25
+ class User < ActiveRecord::Base
26
+ flexible_enum :status do
27
+ active 0
28
+ disabled 1
29
+ pending 2
30
+ end
31
+ end
32
+ ```
33
+
34
+ Option values may be any type.
35
+
36
+ ```ruby
37
+ class Product < ActiveRecord::Base
38
+ flexible_enum :manufacturer do
39
+ honeywell "HON"
40
+ sharp "SHCAY"
41
+ end
42
+ end
43
+ ```
44
+
45
+ ## Working with Values
46
+
47
+ Available options for each attribute are defined as constants on the target class. The classes above would have defined:
48
+
49
+ ```ruby
50
+ User::ACTIVE # => 0
51
+ User::DISABLED # => 1
52
+ User::PENDING # => 2
53
+ Product::HONEYWELL # => "HON"
54
+ Product::SHARP # => "SHCAY"
55
+ ```
56
+
57
+ ## Setter Methods
58
+
59
+ FlexibleEnum adds convenience methods for changing the current value of an attribute and immediately saving it to the database. By default, bang methods are added for each option:
60
+
61
+ ```ruby
62
+ u = User.new
63
+ u.active! # Calls update_attributes(status: 0)
64
+ u.disabled! # Calls update_attributes(status: 1)
65
+ ```
66
+
67
+ The name of the setter method can be changed using option configuration parameters:
68
+
69
+ ```ruby
70
+ class Post < ActiveRecord::Base
71
+ flexible_enum :visibility do
72
+ invisible 0, setter: :hide!
73
+ visible 1, setter: :show!
74
+ end
75
+ end
76
+
77
+ p = Post.new
78
+ p.show! # Calls update_attributes(visibility: 1)
79
+ p.hide! # Calls update_attributes(visibility: 0)
80
+ ```
81
+
82
+ ### Timestamps
83
+
84
+ If the target class defines a date and/or time attribute corresponding to the flexible enum option being set it will be updated with the current date/time when using setter methods. For example, Post#show! above will set `visibility = 1`, `visibile_at = Time.now.utc`, and `visible_on = Time.now.utc.to_date` if those columns exist. The existance of columns is checked using ActiveRecord's `attribute_method?` method.
85
+
86
+ Use the `:timestamp_attribute` option configuration parameter to change the columns used:
87
+
88
+ ```ruby
89
+ flexible_enum :status do
90
+ unknown 0
91
+ active 1, timestamp_attribute: :actived
92
+ disabled 2, timestamp_attribute: :disabled
93
+ end
94
+ ```
95
+
96
+ Calling `active!` will now attempt to set `actived_at` and `actived_on`.
97
+
98
+ ## Predicate Methods
99
+
100
+ FlexibleEnum adds convenience methods for checking whether an option's value is also the attribute's current value.
101
+
102
+ ```ruby
103
+ p = Post.new
104
+ p.show!
105
+ p.visible? # => true
106
+ p.invisible? # => false
107
+ ```
108
+
109
+ Inverse predicate methods can be added by setting the :inverse configuration parameter. Inverse predicate methods have the reverse logic:
110
+
111
+ ```ruby
112
+ class Car < ActiveRecord::Base
113
+ flexible_enum :fuel_type do
114
+ gasoline 0
115
+ diesel 1
116
+ electric 2, inverse: :carbon_emitter
117
+ end
118
+ end
119
+
120
+ c = Car.new
121
+ c.gasoline!
122
+ c.carbon_emitter? # => true
123
+ c.diesel!
124
+ c.carbon_emitter? # => true
125
+ c.electric!
126
+ c.carbon_emitter? # => false
127
+ ```
128
+
129
+ ## Humanized Values
130
+
131
+ Humanized versions of attributes are available. This is convenient for displaying the current value on screen (see "Option Reflection" for rendering drop down lists).
132
+
133
+ ```ruby
134
+ c = Car.new(fuel_type: Car::DIESEL)
135
+ c.human_fuel_type = "Diesel"
136
+ Car.human_fuel_type(0) # => "Gasoline"
137
+ Car.fuel_types.collect(&:human_name) # => ["Gasoline", "Diesel", "Electric"]
138
+ ```
139
+
140
+ If the flexible enum value is `nil`, the humanized name will also be `nil`:
141
+
142
+ ```ruby
143
+ c = Car.new(fuel_type: nil)
144
+ c.human_fuel_type # => nil
145
+ ```
146
+
147
+ ## Name Method
148
+
149
+ The name of the attribute value is available. This allows you to grab the stringified version of the name of the value.
150
+
151
+ ```ruby
152
+ c = Car.new(fuel_type: Car::CARBON_EMITTER)
153
+ c.fuel_type_name # => "carbon_emitter"
154
+ ```
155
+
156
+ If the flexible enum value is `nil`, the name will also be `nil`:
157
+
158
+ ```ruby
159
+ c = Car.new(fuel_type: nil)
160
+ c.fuel_type_name # => nil
161
+ ```
162
+
163
+ ## Namespaced Attributes
164
+
165
+ FlexibleEnum attributes may be namespaced. Adding the namespace option to `flexible_enum` results in constants being defined in a new module.
166
+
167
+ ```ruby
168
+ class CashRegister < ActiveRecord::Base
169
+ flexible_enum :drawer_position, namespace: "DrawerPositions" do
170
+ opened 0
171
+ closed 1
172
+ end
173
+ end
174
+
175
+ # Constants are defined in a new module
176
+ CashRegister::DrawerPositions::OPENED # => 0
177
+ CashRegister::DrawerPositions::CLOSED # => 1
178
+
179
+ # Convenience methods are not affected by namespace
180
+ r = CashRegister.new
181
+ r.opened!
182
+ r.closed!
183
+ ```
184
+
185
+ ## Scopes
186
+
187
+ FlexibleEnum adds ActiveRecord scopes for each attribute option:
188
+
189
+ ```ruby
190
+ User.active # => User.where(status: 0)
191
+ User.disabled # => User.where(status: 1)
192
+ User.pending # => User.where(status: 2)
193
+ ```
194
+
195
+ When an attribute is namespaced a prefix is added to scope names. The prefix is the singularized namespace name (using Active Support):
196
+
197
+ ```ruby
198
+ CashRegister.drawer_position_opened # => CashRegister.where(drawer_position: 0)
199
+ CashRegister.drawer_position_closed # => CashRegister.where(drawer_position: 1)
200
+ ```
201
+
202
+ ## Custom Options
203
+
204
+ Configuration parameters passed to attribute options are saved even if they are unknown. Getting at custom configuration parameters is a little clumsy at the moment but this can still be useful in some cases:
205
+
206
+ ```ruby
207
+ class EmailEvent < ActiveRecord::Base
208
+ flexible_enum :event_type do
209
+ bounce 1, processor_class: RejectedProcessor
210
+ dropped 2, processor_class: RejectedProcessor
211
+ opened 3, processor_class: EmailOpenedProcessor
212
+ delivered 4, processor_class: DeliveryProcessor
213
+ end
214
+
215
+ def process
216
+ class.event_types[event_type][:processor_class].new(self).process
217
+ end
218
+ end
219
+ ```
220
+
221
+ ## Option Introspection
222
+
223
+ You may introspect on available options and their configuration parameters:
224
+
225
+ ```ruby
226
+ ary = EmailEvent.event_types
227
+ ary.collect(&:name) # => ["bounce", "dropped", "opened", "delivered"]
228
+ ary.collect(&:human_name) # => ["Bounce", "Dropped", "Opened", "Delivered"]
229
+ ary.collect(&:value) # => [1, 2, 3, 4]
230
+ ```
231
+
232
+ This works particularly well with ActionView:
233
+
234
+ ```ruby
235
+ f.collection_select(:event_type, EmailEvent.event_types, :value, :human_name)
236
+ ```
237
+
238
+ ## Enum Introspection
239
+
240
+ You may retrieve a list of all defined `flexible_enum`s on a particular class:
241
+
242
+ ```ruby
243
+ class Car < ActiveRecord::Base
244
+ flexible_enum :status do
245
+ new 1
246
+ used 2
247
+ end
248
+
249
+ flexible_enum :car_type do
250
+ gas 1
251
+ hybrid 2
252
+ electric 3
253
+ end
254
+ end
255
+
256
+ Car.flexible_enums # => { status: Car.statuses, car_type: Car.car_types }
257
+ ```
258
+
259
+ ## Overriding Methods
260
+
261
+ You may override any method defined on the target class by FlexibleEnum. In version 0.0.1, `super` behaved as it would without FlexibleEnum being present, you could not call a FlexibleEnum method implementation from an overriding method. As of version 0.0.2, `super` instead references the FlexibleEnum implementation of a method when overriding a FlexibleEnum-defined method.
262
+
263
+ ```ruby
264
+ class Item < ActiveRecord::Base
265
+ flexible_enum :availability do
266
+ discontinued 0
267
+ backorder 1
268
+ in_stock 2
269
+ end
270
+
271
+ # Version 0.0.1
272
+ # Calling super would throw NoMethodError so we'd have to reimplement the method.
273
+ def in_stock!
274
+ BackInStockNotifier.new(self).queue if backorder?
275
+ update_attribute!(status: IN_STOCK)
276
+ end
277
+
278
+ # Version 0.0.2
279
+ # Calling super works and is preferred.
280
+ def in_stock!
281
+ BackInStockNotifier.new(self).queue if backorder?
282
+ super
283
+ end
284
+ end
285
+ ```
286
+
287
+ ## Contributing
288
+
289
+ Please see [CONTRIBUTING.md].
290
+
291
+ ## About MeYou Health
292
+
293
+ ![http://meyouhealth.com/](https://avatars3.githubusercontent.com/u/249181?v=3&s=200)
294
+
295
+ FlexibleEnum is maintained by MeYou Health, LLC.
data/Rakefile ADDED
@@ -0,0 +1,4 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
@@ -0,0 +1,32 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'flexible_enum/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "flexible_enum"
8
+ spec.version = FlexibleEnum::VERSION
9
+
10
+ contributors = `git shortlog -sne`.split("\n").collect { |l| l.scan(/\t(.*) <(.*)>/) }.flatten(1)
11
+ spec.authors = contributors.collect(&:first)
12
+ spec.email = contributors.collect(&:last)
13
+
14
+ spec.description = %q{Helpers for enum-like fields}
15
+ spec.summary = %q{Helpers for enum-like fields}
16
+ spec.homepage = "https://github.com/meyouhealth/flexible_enum"
17
+ spec.license = "MIT"
18
+
19
+ spec.files = `git ls-files`.split($/)
20
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
21
+ spec.test_files = spec.files.grep(%r{^spec/})
22
+ spec.require_paths = ["lib"]
23
+
24
+ spec.add_dependency "activesupport", ">= 3.0"
25
+
26
+ spec.add_development_dependency "appraisal"
27
+ spec.add_development_dependency "bundler", "~> 1.3"
28
+ spec.add_development_dependency "fury"
29
+ spec.add_development_dependency "rake"
30
+ spec.add_development_dependency "rspec"
31
+ spec.add_development_dependency "sqlite3"
32
+ end
@@ -0,0 +1,7 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "http://rubygems.org"
4
+
5
+ gem "rails", "~> 4.1.10"
6
+
7
+ gemspec :path => "../"
@@ -0,0 +1,7 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "http://rubygems.org"
4
+
5
+ gem "rails", "~> 4.2.1"
6
+
7
+ gemspec :path => "../"
@@ -0,0 +1,11 @@
1
+ module FlexibleEnum
2
+ class AbstractConfigurator < Struct.new(:feature_module, :attribute_name, :module_for_elements, :elements)
3
+ def add_class_method(method_name, &block)
4
+ feature_module.const_get(:ClassMethods).send(:define_method, method_name, &block)
5
+ end
6
+
7
+ def add_instance_method(method_name, &block)
8
+ feature_module.send(:define_method, method_name, &block)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,20 @@
1
+ module FlexibleEnum
2
+ class Configuration
3
+ def self.load(&block)
4
+ new.tap {|i| i.instance_eval(&block) }
5
+ end
6
+
7
+ def initialize
8
+ @config = {}
9
+ end
10
+
11
+ def elements
12
+ @config.dup
13
+ end
14
+
15
+ def method_missing(element_name, value, options = {})
16
+ @config[element_name] = options
17
+ @config[element_name][:value] = value
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,11 @@
1
+ module FlexibleEnum
2
+ class ConstantConfigurator < AbstractConfigurator
3
+ def apply
4
+ elements.each do |element_name, element_config|
5
+ constant_name = element_name.to_s.upcase
6
+ constant_value = element_config[:value]
7
+ module_for_elements.const_set(constant_name, constant_value)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,47 @@
1
+ require 'active_support/concern'
2
+ require 'active_support/inflector'
3
+
4
+ module FlexibleEnum
5
+ module Mixin
6
+ extend ActiveSupport::Concern
7
+
8
+ module ClassMethods
9
+ def flexible_enums
10
+ @flexible_enums ||= {}
11
+ end
12
+
13
+ def flexible_enum(attribute_name, attribute_options = {}, &config)
14
+ # Methods are defined on the feature module which in turn is mixed in to the target class
15
+ feature_module = Module.new do |m|
16
+ extend ActiveSupport::Concern
17
+ const_set :ClassMethods, Module.new
18
+ def m.inspect
19
+ "FlexibleEnum(#{self})"
20
+ end
21
+ end
22
+
23
+ # The module that will hold references to value constants
24
+ module_for_elements = attribute_options[:namespace] ? self.const_set(attribute_options[:namespace], Module.new) : feature_module
25
+
26
+ # Read configuration
27
+ elements = Configuration.load(&config).elements
28
+
29
+ # Configure the target object for the given attribute
30
+ configurators = [ConstantConfigurator,
31
+ NameConfigurator,
32
+ QuestionMethodConfigurator,
33
+ SetterMethodConfigurator,
34
+ ScopeConfigurator,
35
+ PotentialValuesConfigurator]
36
+ configurators.each do |configurator|
37
+ configurator.new(feature_module, attribute_name, module_for_elements, elements).apply
38
+ end
39
+
40
+ # Add functionality to target inheritance chain
41
+ send(:include, feature_module)
42
+
43
+ flexible_enums[attribute_name] = public_send("#{attribute_name.to_s.pluralize}_by_sym")
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,44 @@
1
+ module FlexibleEnum
2
+ class NameConfigurator < AbstractConfigurator
3
+ def apply
4
+ configurator = self
5
+
6
+ add_instance_method("#{attribute_name}_name") do
7
+ value = send(configurator.attribute_name)
8
+ configurator.name_for(value)
9
+ end
10
+
11
+ add_class_method("human_#{attribute_name}") do |value|
12
+ configurator.human_name_for(value)
13
+ end
14
+
15
+ add_instance_method("human_#{attribute_name}") do
16
+ value = send(configurator.attribute_name)
17
+ configurator.human_name_for(value)
18
+ end
19
+ end
20
+
21
+ def name_for(value)
22
+ if value
23
+ element_info(value).first.to_s
24
+ else
25
+ nil
26
+ end
27
+ end
28
+
29
+ def human_name_for(value)
30
+ if value
31
+ element_name, element_config = element_info(value)
32
+ element_config[:human_name] || element_name.to_s.humanize
33
+ else
34
+ nil
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ def element_info(value)
41
+ elements.select{|e,c| c[:value] == value }.first
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,45 @@
1
+ module FlexibleEnum
2
+ class PotentialValuesConfigurator < AbstractConfigurator
3
+ def apply
4
+ configurator = self
5
+
6
+ add_class_method(attribute_name.to_s.pluralize) do
7
+ configurator.elements.map(&configurator.option_builder_for_target(self)).sort_by(&:value)
8
+ end
9
+
10
+ add_class_method("#{attribute_name.to_s.pluralize}_by_sym") do
11
+ configurator.elements.inject({}) do |all_options, (element_name, element_config)|
12
+ all_options.merge element_name => configurator.option_builder_for_target(self).call(element_name, element_config)
13
+ end
14
+ end
15
+
16
+ add_class_method("#{attribute_name}_value_for") do |sym_string_or_const|
17
+ element_by_symbol = send(:"#{configurator.attribute_name.to_s.pluralize}_by_sym")[:"#{sym_string_or_const.to_s.downcase}"]
18
+ element_by_value = send(configurator.attribute_name.to_s.pluralize).select { |e| e.value == sym_string_or_const }.first
19
+ (element_by_symbol || element_by_value).try(:value) or raise("Unknown enumeration element: #{sym_string_or_const}")
20
+ end
21
+ end
22
+
23
+ def option_builder_for_target(target_instance)
24
+ proc { |element_name, element_config| Option.new(target_instance, attribute_name, element_name, element_config) }
25
+ end
26
+
27
+ class Option < Struct.new(:target_class, :attribute_name, :element_name, :element_config)
28
+ def name
29
+ element_name.to_s
30
+ end
31
+
32
+ def human_name
33
+ target_class.send("human_#{attribute_name}", value)
34
+ end
35
+
36
+ def value
37
+ element_config[:value]
38
+ end
39
+
40
+ def [](key)
41
+ element_config[key]
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,21 @@
1
+ module FlexibleEnum
2
+ class QuestionMethodConfigurator < AbstractConfigurator
3
+ def apply
4
+ elements.each do |element_name, element_config|
5
+ attribute_name = self.attribute_name
6
+
7
+ # Define question method
8
+ add_instance_method("#{element_name}?") do
9
+ self.send(attribute_name) == element_config[:value]
10
+ end
11
+
12
+ # Define inverse question method (if requested)
13
+ if element_config[:inverse]
14
+ add_instance_method("#{element_config[:inverse]}?") do
15
+ !self.send("#{element_name}?")
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,23 @@
1
+ module FlexibleEnum
2
+ class ScopeConfigurator < AbstractConfigurator
3
+ def apply
4
+ configurator = self
5
+
6
+ elements.each do |element_name, element_config|
7
+ add_class_method(scope_name(element_name)) do
8
+ unscope(:where => configurator.attribute_name).where(configurator.attribute_name => element_config[:value])
9
+ end
10
+ end
11
+ end
12
+
13
+ private
14
+
15
+ def scope_name(option)
16
+ if feature_module == module_for_elements
17
+ option
18
+ else
19
+ "#{module_for_elements.to_s.split('::').last.underscore.singularize}_#{option}"
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,20 @@
1
+ module FlexibleEnum
2
+ class SetterMethodConfigurator < AbstractConfigurator
3
+ def apply
4
+ attribute_name = self.attribute_name
5
+
6
+ elements.each do |element_name, element_config|
7
+ bang_method_name = element_config[:setter] || "#{element_name}!"
8
+ attributes = {attribute_name => element_config[:value]}
9
+ timestamp_attribute_name = element_config[:timestamp_attribute] || element_name
10
+
11
+ add_instance_method(bang_method_name) do
12
+ time = Time.now.utc
13
+ attributes["#{timestamp_attribute_name}_on".to_sym] = time.to_date if self.class.attribute_method?("#{timestamp_attribute_name}_on")
14
+ attributes["#{timestamp_attribute_name}_at".to_sym] = time if self.class.attribute_method?("#{timestamp_attribute_name}_at")
15
+ update_attributes(attributes)
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,3 @@
1
+ module FlexibleEnum
2
+ VERSION = "0.2.2"
3
+ end
@@ -0,0 +1,19 @@
1
+ require 'active_support/lazy_load_hooks'
2
+ require 'active_support/core_ext/object'
3
+
4
+ module FlexibleEnum
5
+ autoload :Mixin, 'flexible_enum/mixin'
6
+ autoload :Configuration, 'flexible_enum/configuration'
7
+ autoload :AbstractConfigurator, 'flexible_enum/abstract_configurator'
8
+ autoload :ConstantConfigurator, 'flexible_enum/constant_configurator'
9
+ autoload :NameConfigurator, 'flexible_enum/name_configurator'
10
+ autoload :QuestionMethodConfigurator, 'flexible_enum/question_method_configurator'
11
+ autoload :SetterMethodConfigurator, 'flexible_enum/setter_method_configurator'
12
+ autoload :ScopeConfigurator, 'flexible_enum/scope_configurator'
13
+ autoload :PotentialValuesConfigurator, 'flexible_enum/potential_values_configurator'
14
+ autoload :Version, 'flexible_enum/version'
15
+ end
16
+
17
+ ActiveSupport.on_load :active_record do
18
+ include FlexibleEnum::Mixin
19
+ end
@@ -0,0 +1,16 @@
1
+ require 'spec_helper'
2
+
3
+ describe "constant definition" do
4
+ it "sets constants for each value choice" do
5
+ expect(CashRegister::UNKNOWN).to eq(0)
6
+ expect(CashRegister::NOT_ACTIVE).to eq(10)
7
+ expect(CashRegister::ACTIVE).to eq(20)
8
+ expect(CashRegister::HONEYWELL).to eq("HON")
9
+ expect(CashRegister::SHARP).to eq("SHCAY")
10
+ end
11
+
12
+ it "sets constants for each namespaced attribute value choice" do
13
+ expect(CashRegister::DrawerPositions::OPENED).to eq(0)
14
+ expect(CashRegister::DrawerPositions::CLOSED).to eq(1)
15
+ end
16
+ end
@@ -0,0 +1,7 @@
1
+ require 'spec_helper'
2
+
3
+ describe "custom options" do
4
+ it "records unknown options for client recall" do
5
+ expect(CashRegister.statuses[1][:my_custom_option]).to eq("Nothing to see here")
6
+ end
7
+ end
@@ -0,0 +1,9 @@
1
+ require "spec_helper"
2
+
3
+ describe "target class exposing flexible_enums" do
4
+ it "allows consumers to find all defined flexible_enums" do
5
+ expect(CashRegister.flexible_enums[:status].keys).to eq([:unknown, :not_active, :active, :alarm, :full, :empty])
6
+ expect(CashRegister.flexible_enums[:drawer_position].keys).to eq([:opened, :closed])
7
+ expect(CashRegister.flexible_enums[:manufacturer].keys).to eq([:honeywell, :sharp])
8
+ end
9
+ end
@@ -0,0 +1,16 @@
1
+ require 'spec_helper'
2
+
3
+ describe "inheritance in target class" do
4
+ it "allows calling super from overwritten methods" do
5
+ register = CashRegister.new
6
+ register.honeywell!
7
+
8
+ def register.sharp!
9
+ before = manufacturer
10
+ super
11
+ [before, manufacturer]
12
+ end
13
+
14
+ expect(register.sharp!).to eq(["HON", "SHCAY"])
15
+ end
16
+ end
@@ -0,0 +1,44 @@
1
+ require 'spec_helper'
2
+
3
+ describe "name values" do
4
+ it "retrieves the name for the current value" do
5
+ register = CashRegister.new
6
+ register.status = CashRegister::UNKNOWN
7
+ expect(register.status_name).to eq("unknown")
8
+ register.status = CashRegister::NOT_ACTIVE
9
+ expect(register.status_name).to eq("not_active")
10
+ register.status = nil
11
+ expect(register.status_name).to be_nil
12
+ end
13
+
14
+ it "retrieves the human name of the current value" do
15
+ register = CashRegister.new
16
+ register.status = CashRegister::UNKNOWN
17
+ expect(register.human_status).to eq("Unknown")
18
+ register.status = CashRegister::NOT_ACTIVE
19
+ expect(register.human_status).to eq("Not active")
20
+ register.status = nil
21
+ expect(register.human_status).to be_nil
22
+ end
23
+
24
+ it "retrieves human names for available options" do
25
+ expect(CashRegister.human_status(CashRegister::UNKNOWN)).to eq("Unknown")
26
+ expect(CashRegister.human_status(CashRegister::NOT_ACTIVE)).to eq("Not active")
27
+ end
28
+
29
+ it "retrieves custom human names when provided" do
30
+ expect(CashRegister.human_status(CashRegister::ALARM)).to eq("Help I'm being robbed!")
31
+ end
32
+
33
+ it "retrieves the human name of the current value of namespaced attributes" do
34
+ opened_register = CashRegister.new.tap {|r| r.drawer_position = CashRegister::DrawerPositions::OPENED }
35
+ closed_register = CashRegister.new.tap {|r| r.drawer_position = CashRegister::DrawerPositions::CLOSED }
36
+ expect(opened_register.human_drawer_position).to eq("Opened")
37
+ expect(closed_register.human_drawer_position).to eq("Closed")
38
+ end
39
+
40
+ it "retrieves human names for known constants of namespaced attributes" do
41
+ expect(CashRegister.human_drawer_position(CashRegister::DrawerPositions::OPENED)).to eq("Opened")
42
+ expect(CashRegister.human_drawer_position(CashRegister::DrawerPositions::CLOSED)).to eq("Closed")
43
+ end
44
+ end
@@ -0,0 +1,42 @@
1
+ require 'spec_helper'
2
+
3
+ describe "reflection of attribute options" do
4
+ it "returns a list of possible elements" do
5
+ expect(CashRegister.drawer_positions.collect(&:name)).to eq(["opened", "closed"])
6
+ expect(CashRegister.drawer_positions.collect(&:human_name)).to eq(["Opened", "Closed"])
7
+ expect(CashRegister.drawer_positions.collect(&:value)).to eq([0, 1])
8
+ end
9
+
10
+ it "finds the element metadata for the option provided by symbol" do
11
+ opened = CashRegister.drawer_positions_by_sym[:opened]
12
+ expect(opened.name).to eq("opened")
13
+ expect(opened.human_name).to eq("Opened")
14
+ expect(opened.value).to eq(0)
15
+
16
+ closed = CashRegister.drawer_positions_by_sym[:closed]
17
+ expect(closed.name).to eq("closed")
18
+ expect(closed.human_name).to eq("Closed")
19
+ expect(closed.value).to eq(1)
20
+ end
21
+
22
+ it "finds the value corresponding to the option provided by its value" do
23
+ expect(CashRegister.status_value_for(CashRegister::ACTIVE)).to eq(CashRegister::ACTIVE)
24
+ end
25
+
26
+ it "finds the value corresponding to the option name provided as a string" do
27
+ expect(CashRegister.status_value_for("active")).to eq(CashRegister::ACTIVE)
28
+ expect(CashRegister.status_value_for("ACTIVE")).to eq(CashRegister::ACTIVE)
29
+ expect(CashRegister.manufacturer_value_for("honeywell")).to eq("HON")
30
+ end
31
+
32
+ it "finds the value for a given option name provided as a symbol" do
33
+ expect(CashRegister.status_value_for(:active)).to eq(CashRegister::ACTIVE)
34
+ expect(CashRegister.drawer_position_value_for(:opened)).to eq(CashRegister::DrawerPositions::OPENED)
35
+ end
36
+
37
+ it "raises an exception for invalid options" do
38
+ expect { CashRegister.status_value_for(666) }.to raise_error("Unknown enumeration element: 666")
39
+ expect { CashRegister.status_value_for("bad_string") }.to raise_error("Unknown enumeration element: bad_string")
40
+ expect { CashRegister.status_value_for(:bad_symbol) }.to raise_error("Unknown enumeration element: bad_symbol")
41
+ end
42
+ end
@@ -0,0 +1,39 @@
1
+ require 'spec_helper'
2
+
3
+ describe "default behavior of flexible_enum" do
4
+ it "adds default predicates that indicate the current value" do
5
+ register = CashRegister.new
6
+ register.status = CashRegister::ACTIVE
7
+ expect(register).to_not be_unknown
8
+ expect(register).to_not be_not_active
9
+ expect(register).to be_active
10
+ end
11
+
12
+ it "adds predicates that indicate the negation of the current value" do
13
+ register = CashRegister.new
14
+ register.status = CashRegister::UNKNOWN
15
+ expect(register).to be_unknown
16
+ expect(register).to_not be_known
17
+ register.status = CashRegister::NOT_ACTIVE
18
+ expect(register).to_not be_unknown
19
+ expect(register).to be_known
20
+ end
21
+
22
+ it "does not set a default value" do
23
+ default = CashRegister.new
24
+ expect(default.status).to be_nil
25
+ expect(default).to_not be_unknown
26
+ expect(default).to_not be_not_active
27
+ expect(default).to_not be_active
28
+ end
29
+
30
+ it "adds predicates that indicate the current value when namespaced" do
31
+ register = CashRegister.new
32
+ register.drawer_position = CashRegister::DrawerPositions::OPENED
33
+ expect(register).to be_opened
34
+ expect(register).to_not be_closed
35
+ register.drawer_position = CashRegister::DrawerPositions::CLOSED
36
+ expect(register).to_not be_opened
37
+ expect(register).to be_closed
38
+ end
39
+ end
@@ -0,0 +1,27 @@
1
+ require 'spec_helper'
2
+
3
+ describe "scopes" do
4
+ it "builds scopes for each element" do
5
+ actives = CashRegister.create!(status: CashRegister::ACTIVE)
6
+ alarms = CashRegister.create!(status: CashRegister::ALARM)
7
+
8
+ expect(CashRegister.active).to contain_exactly(actives)
9
+ expect(CashRegister.alarm).to contain_exactly(alarms)
10
+ expect(CashRegister.unknown).to be_empty
11
+ end
12
+
13
+ it "builds scopes with prefixed names for each namespaced element" do
14
+ opened = CashRegister.new.tap(&:open!)
15
+ closed = CashRegister.new.tap(&:close!)
16
+
17
+ expect(CashRegister.drawer_position_opened).to contain_exactly(opened)
18
+ expect(CashRegister.drawer_position_closed).to contain_exactly(closed)
19
+ end
20
+
21
+ it "builds scopes that aren't affected by default scopes" do
22
+ WithDefaultScope.new.tap(&:active!)
23
+ passive = WithDefaultScope.new.tap(&:passive!)
24
+
25
+ expect(WithDefaultScope.passive).to contain_exactly(passive)
26
+ end
27
+ end
@@ -0,0 +1,48 @@
1
+ require 'spec_helper'
2
+
3
+ describe "setter methods" do
4
+ subject(:register) { CashRegister.new }
5
+
6
+ it "adds default bang methods for setting a new value" do
7
+ expect(register.status).to be_nil
8
+ register.active!
9
+ expect(register.status).to eq(20)
10
+ register.not_active!
11
+ expect(register.status).to eq(10)
12
+ end
13
+
14
+ it "adds custom bang methods for setting a new value" do
15
+ expect(register.drawer_position).to be_nil
16
+ register.open!
17
+ expect(register.drawer_position).to eq(0)
18
+ register.close!
19
+ expect(register.drawer_position).to eq(1)
20
+ end
21
+
22
+ describe "updating database" do
23
+ let!(:now) { Time.now }
24
+ let(:updates) { [] }
25
+
26
+ before { allow(Time).to receive(:now).and_return(now) }
27
+
28
+ it "immediately dispatches a validation-free update" do
29
+ register.active!
30
+ register.close!
31
+
32
+ expect(register).to be_active
33
+ expect(register).to be_closed
34
+ end
35
+
36
+ it "updates default timestamp columns with the current date and time" do
37
+ register.fill!
38
+ expect(register).to be_full
39
+ expect(register.full_at).to eq(now)
40
+ end
41
+
42
+ it "updates custom timestamp columns with the current date and time" do
43
+ register.empty!
44
+ expect(register).to be_empty
45
+ expect(register.emptied_at).to eq(now)
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,58 @@
1
+ require 'flexible_enum'
2
+ require 'active_record'
3
+
4
+ ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
5
+
6
+ RSpec.configure do |config|
7
+ config.around(:each) do |example|
8
+ ActiveRecord::Base.transaction do
9
+ example.run
10
+ raise ActiveRecord::Rollback
11
+ end
12
+ end
13
+ end
14
+
15
+ ActiveRecord::Schema.define do
16
+ create_table "cash_registers" do |t|
17
+ t.integer "status"
18
+ t.datetime "emptied_at"
19
+ t.datetime "emptied_on"
20
+ t.datetime "full_at"
21
+ t.string "manufacturer"
22
+ t.integer "drawer_position"
23
+ end
24
+
25
+ create_table "with_default_scopes" do |t|
26
+ t.integer "status"
27
+ end
28
+ end
29
+
30
+ class CashRegister < ActiveRecord::Base
31
+ flexible_enum :status do
32
+ unknown 0, inverse: :known
33
+ not_active 10, my_custom_option: "Nothing to see here"
34
+ active 20
35
+ alarm 21, human_name: "Help I'm being robbed!"
36
+ full 22, setter: :fill!
37
+ empty 23, timestamp_attribute: :emptied
38
+ end
39
+
40
+ flexible_enum :drawer_position, :namespace => "DrawerPositions" do
41
+ opened 0, setter: :open!
42
+ closed 1, setter: :close!
43
+ end
44
+
45
+ flexible_enum :manufacturer do
46
+ honeywell "HON"
47
+ sharp "SHCAY"
48
+ end
49
+ end
50
+
51
+ class WithDefaultScope < ActiveRecord::Base
52
+ flexible_enum :status do
53
+ active 0
54
+ passive 1
55
+ end
56
+
57
+ default_scope { where(status: ACTIVE) }
58
+ end
metadata ADDED
@@ -0,0 +1,210 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: flexible_enum
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.2
5
+ platform: ruby
6
+ authors:
7
+ - Matt Daubert
8
+ - Alex Robbin
9
+ - Adam Prescott
10
+ - Matthew Daubert
11
+ - Chad Dressler
12
+ - Sean Santry
13
+ - Adam Prescott
14
+ - Sean Santry
15
+ - jon.zeppieri
16
+ - Alex Robbin
17
+ - David C. Goldhirsch
18
+ - David Larrabee
19
+ - Guillermo Guerini
20
+ - Matthew Daubert
21
+ autorequire:
22
+ bindir: bin
23
+ cert_chain: []
24
+ date: 2015-05-06 00:00:00.000000000 Z
25
+ dependencies:
26
+ - !ruby/object:Gem::Dependency
27
+ name: activesupport
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '3.0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - '>='
38
+ - !ruby/object:Gem::Version
39
+ version: '3.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: appraisal
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - '>='
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: bundler
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ~>
59
+ - !ruby/object:Gem::Version
60
+ version: '1.3'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ~>
66
+ - !ruby/object:Gem::Version
67
+ version: '1.3'
68
+ - !ruby/object:Gem::Dependency
69
+ name: fury
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - '>='
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - '>='
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ - !ruby/object:Gem::Dependency
83
+ name: rake
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - '>='
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ type: :development
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - '>='
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
96
+ - !ruby/object:Gem::Dependency
97
+ name: rspec
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - '>='
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ type: :development
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - '>='
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ - !ruby/object:Gem::Dependency
111
+ name: sqlite3
112
+ requirement: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - '>='
115
+ - !ruby/object:Gem::Version
116
+ version: '0'
117
+ type: :development
118
+ prerelease: false
119
+ version_requirements: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - '>='
122
+ - !ruby/object:Gem::Version
123
+ version: '0'
124
+ description: Helpers for enum-like fields
125
+ email:
126
+ - matt.daubert@meyouhealth.com
127
+ - alex.robbin@meyouhealth.com
128
+ - adam@aprescott.com
129
+ - mdaubert@gmail.com
130
+ - chad@dresslerfamily.com
131
+ - sean.santry@meyouhealth.com
132
+ - adam.prescott@meyouhealth.com
133
+ - sean@seansantry.com
134
+ - jon.zeppieri@meyouhealth.com
135
+ - alex@robbinsweb.biz
136
+ - dgoldhirsch@yahoo.com
137
+ - david.larrabee@meyouhealth.com
138
+ - guillermo@gguerini.com
139
+ - mdaubert+github@gmail.com
140
+ executables: []
141
+ extensions: []
142
+ extra_rdoc_files: []
143
+ files:
144
+ - .gitignore
145
+ - .travis.yml
146
+ - Appraisals
147
+ - CONTRIBUTING.md
148
+ - Gemfile
149
+ - LICENSE.txt
150
+ - README.md
151
+ - Rakefile
152
+ - flexible_enum.gemspec
153
+ - gemfiles/rails_4.1.gemfile
154
+ - gemfiles/rails_4.2.gemfile
155
+ - lib/flexible_enum.rb
156
+ - lib/flexible_enum/abstract_configurator.rb
157
+ - lib/flexible_enum/configuration.rb
158
+ - lib/flexible_enum/constant_configurator.rb
159
+ - lib/flexible_enum/mixin.rb
160
+ - lib/flexible_enum/name_configurator.rb
161
+ - lib/flexible_enum/potential_values_configurator.rb
162
+ - lib/flexible_enum/question_method_configurator.rb
163
+ - lib/flexible_enum/scope_configurator.rb
164
+ - lib/flexible_enum/setter_method_configurator.rb
165
+ - lib/flexible_enum/version.rb
166
+ - spec/constant_definition_spec.rb
167
+ - spec/custom_options_spec.rb
168
+ - spec/enum_introspection_spec.rb
169
+ - spec/inheritance_spec.rb
170
+ - spec/name_values_spec.rb
171
+ - spec/potential_values_spec.rb
172
+ - spec/predicate_methods_spec.rb
173
+ - spec/scopes_spec.rb
174
+ - spec/setter_methods_spec.rb
175
+ - spec/spec_helper.rb
176
+ homepage: https://github.com/meyouhealth/flexible_enum
177
+ licenses:
178
+ - MIT
179
+ metadata: {}
180
+ post_install_message:
181
+ rdoc_options: []
182
+ require_paths:
183
+ - lib
184
+ required_ruby_version: !ruby/object:Gem::Requirement
185
+ requirements:
186
+ - - '>='
187
+ - !ruby/object:Gem::Version
188
+ version: '0'
189
+ required_rubygems_version: !ruby/object:Gem::Requirement
190
+ requirements:
191
+ - - '>='
192
+ - !ruby/object:Gem::Version
193
+ version: '0'
194
+ requirements: []
195
+ rubyforge_project:
196
+ rubygems_version: 2.0.14
197
+ signing_key:
198
+ specification_version: 4
199
+ summary: Helpers for enum-like fields
200
+ test_files:
201
+ - spec/constant_definition_spec.rb
202
+ - spec/custom_options_spec.rb
203
+ - spec/enum_introspection_spec.rb
204
+ - spec/inheritance_spec.rb
205
+ - spec/name_values_spec.rb
206
+ - spec/potential_values_spec.rb
207
+ - spec/predicate_methods_spec.rb
208
+ - spec/scopes_spec.rb
209
+ - spec/setter_methods_spec.rb
210
+ - spec/spec_helper.rb