jeffp-enumerated_attribute 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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