flexible_enum 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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