jeffp-enumerated_attribute 0.1.0

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.
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ .DS_Store
2
+ pkg
3
+ rdoc
4
+ coverage
5
+ test/*.log
data/CHANGELOG.rdoc ADDED
@@ -0,0 +1,11 @@
1
+ == master
2
+
3
+ == 0.1.0 / 2009-07-09
4
+
5
+ * Added dynamic predicate method generation
6
+ * Added options handling
7
+ * Added incrementor and decrementor
8
+ * Added enumeration accessor
9
+ * Added accessor and enumeration value definition
10
+ * Added simple attribute initialization
11
+ * Added DSL for short-hand method definition
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2006-2009 Jeff Patmon
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,266 @@
1
+ = enumerated_attribute
2
+
3
+ +enumerated_attribute+ simplifies the definition of enumeration-based attributes, their accessors, initializers and common predicate and support methods.
4
+
5
+ == Resources
6
+
7
+ Development
8
+
9
+ * http://github.com/jeffp/enumerated_attribute
10
+
11
+ Source
12
+
13
+ * git://github.com/jeffp/enumerated_attribute.git
14
+
15
+ == Description
16
+
17
+ Enumerations are a common and useful pattern in programming. Typically, in Ruby,
18
+ enumerated attributes are implemented with strings, symbols or constants. Often the
19
+ developer is burdened with repeatedly defining common methods in support of each
20
+ attribute. Such repetition coding unnecessarily increases
21
+ costs and wastes time.
22
+
23
+ +enumerated_attribute+ simplifies the definition of enumerated attributes by emphasizing
24
+ convention and DRYing the implementation. Repetitive code such as initializers, accessors,
25
+ predicate and support methods are automatically generated, resulting in better
26
+ encapsulation and quicker, more readable code.
27
+
28
+ Features include:
29
+ * Short and simple definition
30
+ * Dynamically-generated support methods
31
+ * Run-time generated attribute predicates
32
+ * Attribute initialization
33
+ * Method definition short-hand (DSL)
34
+
35
+ Examples of the usage patterns for some of the above features are shown below.
36
+
37
+ == Usage
38
+
39
+ === Defining the Attribute
40
+
41
+ Defining an enumerated attribute is as simple as this:
42
+
43
+ require 'enumerated_attribute'
44
+
45
+ class Tractor
46
+ enumerated_attribute :gear, %w(reverse neutral first second over_drive)
47
+
48
+ def initialize
49
+ @gear = :neutral
50
+ end
51
+ end
52
+
53
+ Notice the plugin +enumerated_attribute+ is required at the top of the code.
54
+ The +require+ line must be added at least once at some point in the code.
55
+ It is not included in subsequent examples.
56
+
57
+ The above code uses +enumerated_attribute+ to define an attribute named 'gear' with five enumeration values.
58
+ In general, +enumerated_attribute+ takes three parameters: the name of the attribute, an array of
59
+ enumeration values (either symbols or strings), and an optional hash of options (not shown above). The complete
60
+ form of +enumerated_attribute+ looks like this:
61
+
62
+ enumerated_attribute :name, array_of_enumerations, hash_of_options
63
+
64
+ Defining the attribute :gear has done a number things.
65
+ It has generated an instance variable '@gear', read/write accessors for the
66
+ attribute and three support methods
67
+ for the enumeration, including an incrementor and decrementor. These methods are
68
+ demonstrated below using the Tractor class defined above:
69
+
70
+ Tractor.instance_methods(false)
71
+ # =>["gear", "gear=", "gears", "gear_next", "gear_previous", ...
72
+
73
+ t = Tractor.new
74
+ t.gear # => :neutral
75
+ t.gear= :reverse # => :reverse
76
+ t.gear # => :reverse
77
+ t.gear= :third
78
+ # => ArgumentError: 'third' is not an enumerated value for gear attribute
79
+
80
+ t.gears # => [:reverse, :neutral, :first, :second, :over_drive]
81
+ t.gear_next # => :neutral
82
+ t.gear_previous # => :reverse
83
+ t.gear_previous # => :over_drive
84
+
85
+ The plugin has defined +gear+ and +gear=+ accessors for the attribute. They can be used
86
+ to set the attribute to one of the defined enumeration values. Attempting to set the
87
+ attribute to something besides a defined enumeration value raises an ArgumentError.
88
+
89
+ +gear_next+ and +gear_previous+ are incrementors and decrementors of the attribute.
90
+ The increment order is based on the order of the enumeration values in the attribute definition.
91
+ Both the incrementor and decrementor will wrap when reaching the boundary elements
92
+ of the enumeration array. For example:
93
+
94
+ t.gear = :second
95
+ t.gear_next # => :over_drive
96
+ t.gear_next # => :reverse
97
+
98
+
99
+ ==== Using Attribute Predicates
100
+
101
+ Attribute predicates are methods used to query the state of the attribute,
102
+ for instance, +gear_is_neutral?+ is a predicate method for the gear attribute.
103
+ The plugin will evaluate and respond to predicate methods
104
+ for any attribute defined with it. Predicate methods are not pre-generated. By
105
+ adhering to a specific format, the plugin can recognize attribute predicates
106
+ and generate the methods dynamically. +enumerated_attribute+ recognizes
107
+ methods of the following format:
108
+
109
+ {attribute name}_{anything}_{enumeration value}?
110
+
111
+ The predicate method must satisfy three requirements: it must begin with the name
112
+ of the attribute,
113
+ it must end with a question mark, and the question mark must be preceded with
114
+ a valid enumeration value (all connected by underscores without colons).
115
+ So we can write the following two predicate methods without any prior definition and
116
+ the plugin will recognize, define and respond to them as demonstrated here:
117
+
118
+ t.gear= :neutral
119
+ t.gear_is_in_neutral? # => true
120
+ t.gear_is_in_reverse? # => false
121
+
122
+ The '_is_in_' part of the methods above is merely semantic but enhances
123
+ readability. The contents of the {anything} portion is completely
124
+ at the discretion of the developer. However, there is one twist.
125
+ The evaluation of a predicate method can be negated
126
+ by including 'not' in the the middle {anything} section, such as here:
127
+
128
+ t.gear_is_not_in_neutral? # => false
129
+ t.gear_is_not_in_reverse? # => true
130
+
131
+ Basically, the shortest acceptable form of a predicate method is:
132
+
133
+ t.gear_neutral? # => true
134
+ t.gear_not_neutral? # => false
135
+
136
+
137
+ ==== Initializing Attributes
138
+
139
+ The plugin provides a few ways to eliminate setting the initial value of the attribute in
140
+ the +initialize+ method. Two ways are demonstrated here:
141
+
142
+ class Tractor
143
+ enum_attr :gear, %w(reverse ^neutral first second third)
144
+ enum_attr :front_light, %w(off low high), :init=>:off
145
+ end
146
+
147
+ t=Tractor.new
148
+ t.gear # => :neutral
149
+ t.front_light # => :off
150
+
151
+ *Note* +enumerated_attribute+ can be abbreviated to +enum_attr+. The abbreviated
152
+ form will be used in subsequent examples.
153
+
154
+ The first and simplest way involves designating the initial value by
155
+ prepending a carot '^' to one of the enumeration values in the definition.
156
+ The plugin recognizes that the gear attribute is to be initialized to :neutral.
157
+ Alternatively, the :init option can be used to indicate the initial value of the attribute.
158
+
159
+
160
+ ==== Changing Method Names
161
+
162
+ The plugin provides options for changing the method names of the enumeration accessor, incrementor
163
+ and decrementor (ie, +gears+, +gear_next+, +gear_previous+):
164
+
165
+ class Tractor
166
+ enum_attr :lights, %w(^off low high), :plural=>:lights_values,
167
+ :inc=>'lights_inc', :dec=>'lights_dec'
168
+ end
169
+
170
+ t=Tractor.new
171
+ t.lights_values # => [:off, :low, :high]
172
+ t.lights_inc # => :low
173
+ t.lights_dec # => :off
174
+
175
+ By default, the plugin uses the plural of the attribute for the accessor method name of the enumeration
176
+ values. The pluralization is done simply by adding an 's'. In the case of 'lights' as an
177
+ attribute, the default pluralization does not work, so the accessor can be changed using
178
+ the :plural option. Likewise, the decrementor
179
+ and incrementor have options :decrementor and :incrementor, or :inc and :dec, for changing
180
+ their method names.
181
+
182
+
183
+ === Defining Other Methods
184
+
185
+ In the case that other methods are required to support the attribute,
186
+ the plugin provides a short-hand for defining these methods in the
187
+ +enumerated_attribute+ block.
188
+
189
+ class Tractor
190
+ enum_attr :gear, %(reverse ^neutral first second over_drive) do
191
+ parked? :neutral
192
+ driving? [:first, :second, :third]
193
+ end
194
+ end
195
+
196
+ t=Tractor.new
197
+ t.parked? # => true
198
+ t.driving? # => false
199
+
200
+ Two predicate methods are defined for the 'gear' attribute in the above example using
201
+ the plugin's short-hand.
202
+ The first method, parked?, defines a method which evaluates
203
+ the code {@gear == :neutral}. The second method, driving?, evaluates
204
+ to true if the attribute is set to one of the enumeration values defined in the array
205
+ [:first, :second, :third].
206
+
207
+ For predicate methods requiring fancier logic,
208
+ a block can be used to define the method body.
209
+
210
+ class Tractor
211
+ enum_attr :gear, %(reverse ^neutral first second over_drive) do
212
+ parked? :neutral
213
+ driving? [:first, :second, :third]
214
+ end
215
+ enum_attr :plow, %(^up down), :plural=>:plow_values do
216
+ plowing? { self.gear_is_in_first? && @plow == :down }
217
+ end
218
+ end
219
+
220
+ Here, a method plowing? is true if the gear attribute equates to :first
221
+ and the plow attribute is set to :down. There is
222
+ no short-hand for the block. The code must be complete and evaluate in
223
+ the context of the instance.
224
+
225
+ Method definitions are not limited to predicate methods. Other methods
226
+ can be defined to manipulate the attributes. Here, two methods are defined acting
227
+ as bounded incrementor and decrementor of the gear attribute.
228
+
229
+ class Tractor
230
+ enum_attr :gear, %(reverse ^neutral first second over_drive) do
231
+ parked? :neutral
232
+ driving? [:first, :second, :third]
233
+ upshift { self.gear_is_in_over_drive? ? self.gear : self.gear_next }
234
+ downshift { self.driving? ? self.gear_previous : self.gear }
235
+ end
236
+ end
237
+
238
+ t = Tractor.new
239
+ t.gear # => :neutral
240
+ 10.times { t.upshift }
241
+ t.gear # => :over_drive
242
+ 10.times { t.downshift }
243
+ t.gear # => :neutral
244
+
245
+ Methods +upshift+ and +downshift+ use the automatically generated
246
+ incrementor and decrementor as
247
+ well as a couple predicate methods. +upshift+ increments the gear attribute until
248
+ it reaches over_drive and does not allow a wrap around. +downshift+ decrements
249
+ until the attribute reaches neutral.
250
+
251
+
252
+ == Testing
253
+
254
+ The plugin uses RSpec for testing. Make sure you have the RSpec gem installed:
255
+
256
+ gem install rspec
257
+
258
+ To test the plugin, run:
259
+
260
+ rake spec:test
261
+
262
+
263
+ == Dependencies
264
+
265
+ By default, there are no dependencies.
266
+
data/Rakefile ADDED
@@ -0,0 +1,87 @@
1
+ #require 'rake/testtask'
2
+ require 'rake/rdoctask'
3
+ require 'rake/gempackagetask'
4
+ require 'rake/contrib/sshpublisher'
5
+
6
+ spec = Gem::Specification.new do |s|
7
+ s.name = 'enumerated_attribute'
8
+ s.version = '0.1.0'
9
+ s.platform = Gem::Platform::RUBY
10
+ s.description = 'A enumerated attribute accessor'
11
+ s.summary = 'Defines enumerated attributes, initial state and dynamic state methods.'
12
+
13
+ s.files = FileList['{examples,lib,tasks,spec}/**/*'] + %w(CHANGELOG.rdoc init.rb LICENSE Rakefile README.rdoc .gitignore) - FileList['test/*.log']
14
+ s.require_path = 'lib'
15
+ s.has_rdoc = true
16
+ s.test_files = Dir['spec/**/*_spec.rb']
17
+
18
+ s.author = 'Jeff Patmon'
19
+ s.email = 'jpatmon@yahoo.com'
20
+ s.homepage = 'http://www.jpatmon.com'
21
+ end
22
+
23
+ desc 'Default: run all tests.'
24
+ task :default => :test
25
+
26
+ require 'spec/version'
27
+ require 'spec/rake/spectask'
28
+
29
+ namespace :spec do
30
+ desc "Run all specs"
31
+ Spec::Rake::SpecTask.new('test') do |t|
32
+ t.spec_files = FileList['spec/**/*_spec.rb']
33
+ #t.spec_opts = ['--options', 'spec/spec.opts']
34
+ t.rcov = false
35
+ #t.rcov_dir = 'coverage'
36
+ #t.rcov_opts = ['--exclude', "kernel,load-diff-lcs\.rb,instance_exec\.rb,lib/spec.rb,lib/spec/runner.rb,^spec/*,bin/spec,examples,/gems,/Library/Ruby,\.autotest,#{ENV['GEM_HOME']}"]
37
+ end
38
+ end
39
+
40
+
41
+ =begin
42
+ desc "Test the #{spec.name} plugin."
43
+ Rake::TestTask.new(:test) do |t|
44
+ t.libs << 'lib'
45
+ t.libs << 'spec'
46
+ t.test_files = spec.test_files
47
+ t.verbose = true
48
+ end
49
+
50
+ begin
51
+ require 'rcov/rcovtask'
52
+ namespace :test do
53
+ desc "Test the #{spec.name} plugin with Rcov."
54
+ Rcov::RcovTask.new(:rcov) do |t|
55
+ t.libs << 'lib'
56
+ t.test_files = spec.test_files
57
+ t.rcov_opts << '--exclude="^(?!lib/)"'
58
+ t.verbose = true
59
+ end
60
+ end
61
+ rescue LoadError
62
+ end
63
+ =end
64
+
65
+ desc "Generate documentation for the #{spec.name} plugin."
66
+ Rake::RDocTask.new(:rdoc) do |rdoc|
67
+ rdoc.rdoc_dir = 'rdoc'
68
+ rdoc.title = spec.name
69
+ #rdoc.template = '../rdoc_template.rb'
70
+ rdoc.options << '--line-numbers' << '--inline-source'
71
+ rdoc.rdoc_files.include('README.rdoc', 'CHANGELOG.rdoc', 'LICENSE', 'lib/**/*.rb')
72
+ end
73
+
74
+ desc 'Generate a gemspec file.'
75
+ task :gemspec do
76
+ File.open("#{spec.name}.gemspec", 'w') do |f|
77
+ f.write spec.to_ruby
78
+ end
79
+ end
80
+
81
+ Rake::GemPackageTask.new(spec) do |p|
82
+ p.gem_spec = spec
83
+ p.need_tar = true
84
+ p.need_zip = true
85
+ end
86
+
87
+ Dir['tasks/**/*.rake'].each {|rake| load rake}
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'enumerated_attribute'
@@ -0,0 +1,274 @@
1
+ module EnumeratedAttribute
2
+
3
+ #todo: system wide constants
4
+ #todo: allow nil
5
+ #todo: setter_callback
6
+ #todo: ArgumentError may need to use Errors for ActiveRecord
7
+
8
+ def enumerated_attribute(*args, &block)
9
+ return if args.empty?
10
+ attr_name = args[0].to_s
11
+ attr_sym = attr_name.to_sym
12
+ enums = (args[1] && args[1].instance_of?(Array) ? args[1] : [])
13
+ index = enums.empty? ? 1 : 2
14
+ opts = (args[index] && args[index].instance_of?(Hash) ? args[index] : {})
15
+
16
+ raise(ArgumentError, 'second argument of enumerated_attribute/enum_attr is not an array of symbols or strings representing the enum values', caller) if enums.empty?
17
+
18
+ #todo: better pluralization of attribute
19
+ initial_value = nil
20
+ plural_name = opts[:plural] || opts[:enums_accessor] || opts[:enums] || "#{attr_name}s"
21
+ incrementor = opts[:incrementor] || opts[:inc] || "#{attr_name}_next"
22
+ decrementor = opts[:decrementor] || opts[:dec] || "#{attr_name}_previous"
23
+
24
+ class_eval <<-ATTRIB
25
+ @@enumerated_attribute_names ||= []
26
+ @@enumerated_attribute_names << '#{attr_name}'
27
+ ATTRIB
28
+
29
+ unless enums.empty?
30
+ enums = enums.map{|v| (v =~ /^\^/ ? (initial_value = v[1, v.length-1].to_sym) : v.to_sym )}
31
+ class_eval <<-ENUMS
32
+ @@enumerated_attribute_values ||= {}
33
+ @@enumerated_attribute_values[:#{attr_name}] = [:#{enums.join(',:')}]
34
+ ENUMS
35
+ end
36
+
37
+ #create accessors
38
+ attr_reader attr_sym
39
+ if enums.empty?
40
+ attr_writer attr_sym
41
+ else
42
+ enumerated_attr_writer attr_sym
43
+ end
44
+
45
+ #define dynamic methods in method_missing
46
+ class_eval <<-METHOD
47
+ unless @missing_method_for_enumerated_attribute_defined
48
+ if method_defined?(:method_missing)
49
+ alias_method(:method_missing_without_enumerated_attribute, :method_missing)
50
+ def method_missing(methId, *args, &block)
51
+ return self.send(methId) if define_enumerated_attribute_dynamic_method(methId)
52
+ method_missing_without_enumerated_attribute(methId, *args, &block)
53
+ end
54
+ else
55
+ def method_missing(methId, *args, &block)
56
+ return self.send(methId) if define_enumerated_attribute_dynamic_method(methId)
57
+ super
58
+ end
59
+ end
60
+ @missing_method_for_enumerated_attribute_defined = true
61
+
62
+ alias_method :respond_to_without_enumerated_attribute?, :respond_to?
63
+ def respond_to?(method)
64
+ respond_to_without_enumerated_attribute?(method) || !!parse_dynamic_method_parts(method.to_s)
65
+ end
66
+
67
+ private
68
+
69
+ def parse_dynamic_method_parts(meth_name)
70
+ return(nil) unless meth_name[-1, 1] == '?'
71
+
72
+ meth_name.chop! #remove the ?
73
+
74
+ attr = nil
75
+ @@enumerated_attribute_names.each do |name|
76
+ if meth_name.sub!(Regexp.new("^"+name.to_s), "")
77
+ attr = name; break
78
+ end
79
+ end
80
+ return (nil) unless attr
81
+ attr_sym = attr.to_sym
82
+
83
+ value = nil
84
+ if @@enumerated_attribute_values.key?(attr_sym)
85
+ @@enumerated_attribute_values[attr_sym].each do |v|
86
+ if meth_name.sub!(Regexp.new(v.to_s+"$"), "")
87
+ value = v; break
88
+ end
89
+ end
90
+ end
91
+ return (nil) unless value
92
+
93
+ [attr, value, meth_name]
94
+ end
95
+
96
+ def define_enumerated_attribute_dynamic_method(methId)
97
+ meth_name = methId.id2name
98
+ return(false) unless (parts = parse_dynamic_method_parts(meth_name))
99
+ return(false) unless parts
100
+
101
+ negated = !!parts[2].downcase.match(/_not_/)
102
+ self.class.define_attribute_state_method(methId, parts[0], parts[1].to_sym, negated)
103
+
104
+ true
105
+ end
106
+ end
107
+ METHOD
108
+
109
+ #create state and action methods from block
110
+ initial_value = opts[:init] || initial_value
111
+ if block_given?
112
+ m = EnumeratedAttribute::MethodDefinitionDSL.new(self, attr_name, enums)
113
+ m.instance_eval(&block)
114
+ initial_value = m.initial_value || initial_value
115
+ plural_name = m.pluralized_name || plural_name
116
+ decrementor = m.decrementor_name || decrementor
117
+ incrementor = m.incrementor_name || incrementor
118
+ end
119
+
120
+ #define the enum values accessor
121
+ unless enums.empty?
122
+ class_eval <<-ENUM
123
+ def #{plural_name}
124
+ @@enumerated_attribute_values[:#{attr_name}]
125
+ end
126
+ def #{incrementor}
127
+ z = @@enumerated_attribute_values[:#{attr_name}]
128
+ index = z.index(@#{attr_name})
129
+ @#{attr_name} = z[index >= z.size-1 ? 0 : index+1]
130
+ end
131
+ def #{decrementor}
132
+ z = @@enumerated_attribute_values[:#{attr_name}]
133
+ index = z.index(@#{attr_name})
134
+ @#{attr_name} = z[index > 0 ? index-1 : z.size-1]
135
+ end
136
+ ENUM
137
+ end
138
+
139
+ #establish initial value
140
+ if (initial_value = opts[:init] || initial_value)
141
+ class_eval <<-INITVAL
142
+ unless @enumerated_attribute_init
143
+ @enumerated_attribute_init = {}
144
+ class << self
145
+ def new_with_enumerated_attribute(*args)
146
+ result = new_without_enumerated_attribute(*args)
147
+ @enumerated_attribute_init.each do |k,v|
148
+ result.instance_variable_set("@"+k.to_s, v)
149
+ end
150
+ result
151
+ end
152
+ alias_method :new_without_enumerated_attribute, :new
153
+ alias_method :new, :new_with_enumerated_attribute
154
+ end
155
+ end
156
+ @enumerated_attribute_init[:#{attr_name}] = :#{initial_value}
157
+ INITVAL
158
+ end
159
+
160
+ end
161
+
162
+ #a short cut
163
+ alias :enum_attr :enumerated_attribute
164
+
165
+ def define_attribute_state_method(symbol, attr_name, value, negated)
166
+ define_method symbol do
167
+ ival = instance_variable_get('@'+attr_name)
168
+ negated ? ival != value : ival == value
169
+ end
170
+ end
171
+
172
+ private
173
+
174
+ def enumerated_attr_writer name
175
+ name = name.to_s
176
+ class_eval <<-METHOD
177
+ def #{name}=(val)
178
+ val = val.to_sym if val.instance_of?(String)
179
+ raise(ArgumentError, "'" + val.to_s + "' is not an enumerated value for #{name} attribute", caller) unless @@enumerated_attribute_values[:#{name}].include?(val)
180
+ @#{name} = val
181
+ end
182
+ METHOD
183
+ end
184
+
185
+ public
186
+
187
+ class MethodDefinitionDSL
188
+ attr_reader :initial_value, :pluralized_name, :decrementor_name, :incrementor_name
189
+
190
+ def initialize(class_obj, attr_name, values=[])
191
+ @class_obj = class_obj
192
+ @attr_name = attr_name
193
+ @attr_values = values
194
+ end
195
+
196
+ def method_missing(methId, *args, &block)
197
+ meth_name = methId.id2name
198
+ if meth_name[-1,1] != '?'
199
+ #not a state method - this must include either a proc or block - no short cuts
200
+ if args.size > 0
201
+ arg = args[0]
202
+ if arg.instance_of?(Proc)
203
+ @class_obj.send(:define_method, methId, arg)
204
+ return
205
+ end
206
+ elsif block_given?
207
+ @class_obj.send(:define_method, methId, block)
208
+ return
209
+ end
210
+ raise ArgumentError, "method '#{meth_name}' must be followed by a proc or block", caller
211
+ end
212
+
213
+ if (args.size > 0)
214
+ arg = args[0]
215
+ case arg
216
+ when Symbol
217
+ return create_method_from_symbol_or_string(meth_name, arg)
218
+ when String
219
+ return create_method_from_symbol_or_string(meth_name, arg)
220
+ when Array
221
+ return create_method_from_array(meth_name, arg)
222
+ when Proc
223
+ @class_obj.send(:define_method, methId, arg)
224
+ return
225
+ end
226
+ elsif block_given?
227
+ @class_obj.send(:define_method, methId, block)
228
+ return
229
+ end
230
+ raise ArgumentError , "method '#{meth_name}' for :#{@attr_name} attribute must be followed by a symbol, array, proc or block", caller
231
+ end
232
+
233
+ def init(value)
234
+ if (!@attr_values.empty? && !@attr_values.include?(value.to_sym))
235
+ raise(NameError, "'#{value}' in method 'init' is not an enumeration value for :#{@attr_name} attribute", caller)
236
+ end
237
+ @initial_value = value
238
+ end
239
+
240
+ def decrementor(value); @decrementor_name = value; end
241
+ def incrementor(value); @incrementor_name = value; end
242
+ def enums_accessor(value); @pluralized_name = value; end
243
+ alias :inc :incrementor
244
+ alias :dec :decrementor
245
+ alias :enums :enums_accessor
246
+ alias :plural :enums_accessor
247
+
248
+ private
249
+
250
+ def create_method_from_symbol_or_string(meth_name, arg)
251
+ if (!@attr_values.empty? && !@attr_values.include?(arg.to_sym))
252
+ raise(NameError, "'#{arg}' in method '#{meth_name}' is not an enumeration value for :#{@attr_name} attribute", caller)
253
+ end
254
+ @class_obj.class_eval("def #{meth_name}; @#{@attr_name} == :#{arg}; end")
255
+ end
256
+
257
+ def create_method_from_array(meth_name, arg)
258
+ if !@attr_values.empty?
259
+ arg.each do |m|
260
+ if !@attr_values.include?(m.to_sym)
261
+ raise(NameError, "'#{m}' in method '#{meth_name}' is not an enumeration value for :#{@attr_name} attribute", caller)
262
+ end
263
+ end
264
+ end
265
+ @class_obj.class_eval("def #{meth_name}; [:#{arg.join(',:')}].include?(@#{@attr_name}); end")
266
+ end
267
+ end
268
+ end
269
+
270
+ class Class
271
+ include EnumeratedAttribute
272
+ end
273
+
274
+
data/spec/car.rb ADDED
@@ -0,0 +1,37 @@
1
+ require 'enumerated_attribute'
2
+
3
+ #used to test that method_missing chaining plays nice in inheritance situations
4
+
5
+ class Vehicle
6
+
7
+ attr_accessor :vehicle_method_hit
8
+
9
+ def initialize
10
+ @vehicle_method_hit = false
11
+ end
12
+
13
+ def method_missing(methId, *args)
14
+ @vehicle_method_hit = true
15
+ #end here
16
+ end
17
+
18
+ alias :vmh :vehicle_method_hit
19
+
20
+ end
21
+
22
+ class Car < Vehicle
23
+ attr_accessor :car_method_hit
24
+
25
+ def initialize
26
+ super
27
+ @car_method_hit = false
28
+ end
29
+
30
+ def method_missing(methId, *args)
31
+ @car_method_hit = true
32
+ super
33
+ end
34
+
35
+ enum_attr :gear, %w(reverse ^neutral drive)
36
+ alias :cmt :car_method_hit
37
+ end
data/spec/car_spec.rb ADDED
@@ -0,0 +1,21 @@
1
+ require 'spec/car'
2
+
3
+ #used to test that method_missing chaining plays nice in inheritance situation
4
+
5
+ describe "Car" do
6
+
7
+ it "should not hit Car method_missing when calling dynamic state method :gear_is_in_reverse?" do
8
+ c = Car.new
9
+ c.gear_is_in_reverse?
10
+ c.car_method_hit.should be_false
11
+ c.vehicle_method_hit.should be_false
12
+ end
13
+
14
+ it "should hit Car and Vehicle method_missing when not calling supported dynamic state method" do
15
+ c = Car.new
16
+ c.parking_break_is_on?
17
+ c.car_method_hit.should be_true
18
+ c.vehicle_method_hit.should be_true
19
+ end
20
+
21
+ end
data/spec/tractor.rb ADDED
@@ -0,0 +1,38 @@
1
+ require 'enumerated_attribute'
2
+
3
+ class Tractor
4
+
5
+ GEAR_ENUM_VALUES = %w(reverse neutral first second over_drive).map{|v| v.to_sym}
6
+ LIGHTS_ENUM_VALUES = %w(off low high).map{|v| v.to_sym}
7
+ SIDE_LIGHT_ENUM_VALUES = [:off,:low,:high,:super_high]
8
+
9
+ attr_accessor :name
10
+
11
+ def initialize
12
+ @name = 'old faithful'
13
+ end
14
+
15
+ enumerated_attribute :gear, %w(reverse ^neutral first second over_drive) do
16
+ parked? :neutral
17
+ driving? [:first, :second, :over_drive]
18
+ upshift { self.gear_is_in_over_drive? ? self.gear : self.gear_next }
19
+ downshift { self.driving? ? self.gear_previous : self.gear }
20
+ end
21
+
22
+ enum_attr :plow, %w(^up down) do
23
+ plowing? { self.gear_is_in_first? && self.plow == :down }
24
+ end
25
+
26
+ enum_attr :lights, LIGHTS_ENUM_VALUES, :enums_accessor=>:lights_enums, :init=>:off, :decrementor=>:turn_lights_down, :incrementor=>:turn_lights_up do
27
+ lights_are_on? [:low, :high]
28
+ lights_are_not_on? :off
29
+ end
30
+
31
+ enum_attr :side_light, %w(off low high super_high) do
32
+ init :off
33
+ enums_accessor :side_light_enums
34
+ incrementor :side_light_up
35
+ decrementor :side_light_down
36
+ end
37
+
38
+ end
@@ -0,0 +1,239 @@
1
+ require 'spec/tractor'
2
+
3
+ describe "Tractor" do
4
+
5
+ it "should have respond_to? method for :gear_is_in_neutral?" do
6
+ t=Tractor.new
7
+ t.respond_to?('gear_is_in_neutral?').should be_true
8
+ end
9
+
10
+ it "should have respond_to? method for :side_light_is_super_high?" do
11
+ t=Tractor.new
12
+ t.respond_to?(:side_light_is_super_high?).should be_true
13
+ end
14
+
15
+ it "should not have respond_to? method for :gear_is_in_high?" do
16
+ t=Tractor.new
17
+ t.respond_to?(:gear_is_in_high?).should be_false
18
+ end
19
+
20
+ it "should initially set :plow to :up" do
21
+ t=Tractor.new
22
+ t.plow.should == :up
23
+ end
24
+
25
+ it "should have plowing? state method" do
26
+ t=Tractor.new
27
+ t.plowing?.should be_false
28
+ t.plow=:down
29
+ t.plowing?.should be_false
30
+ t.gear= :first
31
+ t.plowing?.should be_true
32
+ t.plow=:up
33
+ t.plowing?.should be_false
34
+ end
35
+
36
+ it "should have :side_light_up incrementor" do
37
+ t=Tractor.new
38
+ t.side_light = :off
39
+ t.side_light_up.should == :low
40
+ t.side_light_up.should == :high
41
+ t.side_light_up.should == :super_high
42
+ t.side_light_up.should == :off
43
+ end
44
+
45
+ it "should have :side_light_down incrementor" do
46
+ t=Tractor.new
47
+ t.side_light_down.should == :super_high
48
+ t.side_light_down.should == :high
49
+ t.side_light_down.should == :low
50
+ t.side_light_down.should == :off
51
+ end
52
+
53
+ it "should have :turn_lights_up incrementor" do
54
+ t=Tractor.new
55
+ t.lights = :off
56
+ t.turn_lights_up.should == :low
57
+ t.turn_lights_up.should == :high
58
+ end
59
+
60
+ it "should have :turn_lights_down decrementor" do
61
+ t=Tractor.new
62
+ t.lights=:high
63
+ t.turn_lights_down.should == :low
64
+ t.turn_lights_down.should == :off
65
+ end
66
+
67
+ it "should have :gear_previous which wraps from :neutral to :over_drive" do
68
+ t=Tractor.new
69
+ t.gear_previous.should == :reverse
70
+ t.gear.should == :reverse
71
+ t.gear_previous.should == :over_drive
72
+ t.gear.should == :over_drive
73
+ end
74
+
75
+ it "should have :gear_next which wraps from :second to :reverse" do
76
+ t=Tractor.new
77
+ t.gear = :second
78
+ t.gear_next.should == :over_drive
79
+ t.gear.should == :over_drive
80
+ t.gear_next.should == :reverse
81
+ t.gear.should == :reverse
82
+ end
83
+
84
+ it "should have :upshift which increments :gear from :neutral to :over_drive without wrapping" do
85
+ t=Tractor.new
86
+ t.upshift.should == :first
87
+ t.upshift.should == :second
88
+ t.upshift.should == :over_drive
89
+ t.upshift.should == :over_drive
90
+ end
91
+
92
+ it "should have :downshift which decrements :gear from :over_drive to :neutral without wrapping" do
93
+ t=Tractor.new
94
+ t.gear = :over_drive
95
+ t.downshift.should == :second
96
+ t.downshift.should == :first
97
+ t.downshift.should == :neutral
98
+ t.downshift.should == :neutral
99
+ end
100
+
101
+ it "should have parked? method" do
102
+ t=Tractor.new
103
+ t.parked?.should be_true
104
+ t.gear = :reverse
105
+ t.parked?.should be_false
106
+ end
107
+
108
+ it "should have driving? method" do
109
+ t=Tractor.new
110
+ t.driving?.should be_false
111
+ [:first, :second, :over_drive].each do |g|
112
+ t.gear=g
113
+ t.driving?.should be_true
114
+ end
115
+ end
116
+
117
+ it "should initially set side_light to :off" do
118
+ t=Tractor.new
119
+ t.side_light.should == :off
120
+ end
121
+
122
+ it "should have side_light_enums method" do
123
+ t = Tractor.new
124
+ t.side_light_enums.should == Tractor::SIDE_LIGHT_ENUM_VALUES
125
+ end
126
+
127
+ it "should have state method side_light_is_off?" do
128
+ t=Tractor.new
129
+ t.side_light_is_off?.should be_true
130
+ end
131
+
132
+ it "should have state method side_light_is_super_high?" do
133
+ t=Tractor.new
134
+ t.side_light_is_super_high?.should be_false
135
+ end
136
+
137
+ it "should initially set :gear to :neutral" do
138
+ t=Tractor.new
139
+ t.gear.should == :neutral
140
+ end
141
+
142
+ it "should set lights initially to :off" do
143
+ t=Tractor.new
144
+ t.lights.should == :off
145
+ end
146
+
147
+ it "should create a lights_enums method for all light enumerated values" do
148
+ t=Tractor.new
149
+ t.lights_enums.should == Tractor::LIGHTS_ENUM_VALUES
150
+ end
151
+
152
+ it "should initially set lights to :off" do
153
+ t=Tractor.new
154
+ t.lights.should equal(:off)
155
+ end
156
+
157
+ it "should have dynamic state methods for :reverse and :neutral" do
158
+ t = Tractor.new
159
+ t.gear_is_in_reverse?.should be_false
160
+ t.gear_is_in_neutral?.should be_true
161
+ end
162
+
163
+ it "should have negative dynamic state methods for :reverses and :neutral" do
164
+ t = Tractor.new
165
+ t.gear_is_not_in_reverse?.should be_true
166
+ t.gear_is_not_in_neutral?.should be_false
167
+ end
168
+
169
+ it "should have negative and positive dynamic state methods for :over_drive" do
170
+ t = Tractor.new
171
+ t.gear_is_in_over_drive?.should be_false
172
+ t.gear_is_not_in_over_drive?.should be_true
173
+ end
174
+
175
+ it "should have created instance methods for :reverse" do
176
+ m = Tractor.instance_methods(false)
177
+ m.should include('gear_is_in_reverse?')
178
+ m.should include('gear_is_not_in_reverse?')
179
+ end
180
+
181
+ it "should have created instance methods for :neutral" do
182
+ m = Tractor.instance_methods(false)
183
+ m.should include('gear_is_in_neutral?')
184
+ m.should include('gear_is_not_in_neutral?')
185
+ end
186
+
187
+ it "should have created instance methods for :over_drive" do
188
+ m = Tractor.instance_methods(false)
189
+ m.should include('gear_is_in_over_drive?')
190
+ m.should include('gear_is_not_in_over_drive?')
191
+ end
192
+
193
+ it "should raise NoMethodError for dynamic state methods not querying valid enumeration values" do
194
+ t = Tractor.new
195
+ lambda { t.gear_is_in_high? }.should raise_error(NoMethodError)
196
+ end
197
+
198
+ it "should convert string values to symbols for attr setters" do
199
+ t = Tractor.new
200
+ t.gear= 'reverse'
201
+ t.gear.should == :reverse
202
+ end
203
+
204
+ it "should have class variable @@enumerated_attribute_names" do
205
+ Tractor.class_variable_defined?('@@enumerated_attribute_names').should be_true
206
+ end
207
+
208
+ it "should have instance method gears equal to enumeration array" do
209
+ Tractor.new.gears.should == Tractor::GEAR_ENUM_VALUES
210
+ end
211
+
212
+ it "should have gear attribute initialized to :neutral" do
213
+ t = Tractor.new
214
+ t.gear.should == :neutral
215
+ end
216
+
217
+ it "should set gear attribute to :first" do
218
+ t = Tractor.new
219
+ t.gear = :first
220
+ t.gear.should == :first
221
+ end
222
+
223
+ it "should raise error when set gear attribute to :broken" do
224
+ t = Tractor.new
225
+ lambda { t.gear= :broken }.should raise_error(ArgumentError)
226
+ end
227
+
228
+ it "should have name attribute initially set to 'old faithful'" do
229
+ t = Tractor.new
230
+ t.name.should == 'old faithful'
231
+ end
232
+
233
+ it "should set name attribute to 'broke n busted'" do
234
+ t = Tractor.new
235
+ t.name = 'broke n busted'
236
+ t.name.should == 'broke n busted'
237
+ end
238
+
239
+ end
metadata ADDED
@@ -0,0 +1,64 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: jeffp-enumerated_attribute
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Jeff Patmon
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-07-09 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description: A enumerated attribute accessor
17
+ email: jpatmon@yahoo.com
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files: []
23
+
24
+ files:
25
+ - lib/enumerated_attribute.rb
26
+ - spec/car.rb
27
+ - spec/car_spec.rb
28
+ - spec/tractor.rb
29
+ - spec/tractor_spec.rb
30
+ - CHANGELOG.rdoc
31
+ - init.rb
32
+ - LICENSE
33
+ - Rakefile
34
+ - README.rdoc
35
+ - .gitignore
36
+ has_rdoc: false
37
+ homepage: http://www.jpatmon.com
38
+ post_install_message:
39
+ rdoc_options: []
40
+
41
+ require_paths:
42
+ - lib
43
+ required_ruby_version: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: "0"
48
+ version:
49
+ required_rubygems_version: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: "0"
54
+ version:
55
+ requirements: []
56
+
57
+ rubyforge_project:
58
+ rubygems_version: 1.2.0
59
+ signing_key:
60
+ specification_version: 3
61
+ summary: Defines enumerated attributes, initial state and dynamic state methods.
62
+ test_files:
63
+ - spec/car_spec.rb
64
+ - spec/tractor_spec.rb