elemental 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,364 @@
1
+ = ELEMENTAL
2
+
3
+ Elemental gives you enumerated sets. You code symbolically,
4
+ keeping literals away from your conditional logic.
5
+
6
+ == REQUIREMENTS:
7
+
8
+ * Ruby 1.8+
9
+ * Ruby Gems
10
+
11
+ == INSTALL:
12
+
13
+ Project Hosts:
14
+ * http://elemental.rubyforge.net
15
+ * http://github.com/mwlang/elemental/tree/master
16
+
17
+ From Gem:
18
+ sudo gem install elemental
19
+
20
+ From Source:
21
+ gem install bones, rake, test-unit
22
+ git clone git://github.com/mwlang/elemental.git
23
+ cd elemental
24
+ rake gem:package
25
+ cd pkg
26
+ sudo gem install elemental
27
+
28
+ Working Examples:
29
+ Check out the projects in the examples folder for working examples of
30
+ using Elemental. Examples may be limited presently, but more are planned.
31
+ If you have a unique/interesting application of Elemental, please feel free
32
+ to contact me and let me know about it for possible inclusion.
33
+
34
+ == FEATURES/PROBLEMS:
35
+
36
+ * Code with symbols, easily display what the user needs to see.
37
+ * Associate any number of values/attributes with a symbolic element.
38
+ * Change the display values without worrying about logical side-effects.
39
+ * Access elements in a variety of ways: symbol, constant, index.
40
+ * Store in database as either string (the default) or ordinal value.
41
+ * Elemental is also Enumerable. You can iterate all members and sort, too.
42
+ * A default flag can be set that makes it easier to render views.
43
+
44
+ == DESCRIPTION:
45
+
46
+ Elemental provides enumerated collection of elements that allow you to associate
47
+ ruby symbols to arbitrary "display" values, thus allowing your code to "think"
48
+ symbolically and unambiguously while giving you the means to easily display what
49
+ end-users need to see. Additionally, symbols are associated with ordinal values,
50
+ allowing easy storage/retrieval to persistent stores (databases, files,
51
+ marshalling, etc) by saving the Element#value as appropriate (String by default,
52
+ Fixnum if you "persist_ordinally").
53
+
54
+ The primary aim of Elemental is to collect and abstract literals away
55
+ from your code logic. There's an old programmer's wisdom that you should not
56
+ encode your logic using literal values, especially those the end-user is exposed to.
57
+
58
+ Yet, interpreted languages seem to be famous for sowing the practice of doing
59
+ boolean expressions replete with string literals. Which can be ok until one day
60
+ your client says, "I don't like 'Active' can we use 'enabled' instead?").
61
+
62
+ In your code, instead of:
63
+
64
+ if my_user.status == 'Active' ...
65
+ or worse
66
+ my_user.favorite_color = 1
67
+
68
+ (what color is 1?!)
69
+
70
+ The above suffers from the following:
71
+ - literals are subject to change or are cryptic.
72
+ - diff spellings mean diff things ('Active' != 'active' != 'Actve').
73
+ - relying on literals error prone and hard to test.
74
+ - code is often not readable as-is (what color is 1, again?).
75
+
76
+ With Elemental, do this:
77
+
78
+ if my_user.status == UserStatus::active.value
79
+ my_user.favorite_color = Color::blue
80
+
81
+ When you save a value to a persistent store (database, stream, file, etc.), you
82
+ assign the Element#value like so:
83
+
84
+ my_user.status = UserStatus::active.value
85
+
86
+ (more on what you get via "value" later)
87
+
88
+ If UserStatus::active isn't defined, you get an error immediately that you can
89
+ attribute to a typo in your code. This behavior is by design in Elemental.
90
+ That's a unit test you *don't* have to write! Simply define your class or module
91
+ and include the Ruby symbols you need:
92
+
93
+ class AccountStatus
94
+ extend Elemental
95
+ member :active, :display => 'Active'
96
+ member :delinquent, :display => "Account Past Due"
97
+ member :inactive :display => "Inactive"
98
+ end
99
+
100
+ Elemental can return the Element#value as either a Ruby symbol (Symbol) or an
101
+ ordinal (Fixnum) value. The default is a Ruby symbol. To override this and
102
+ return an ordinal value, use "persist_ordinally" as follows:
103
+
104
+ class AccountStatus
105
+ extend Elemental
106
+ persist_ordinally
107
+
108
+ member :active, :display => 'Active'
109
+ member :delinquent, :display => "Account Past Due"
110
+ member :inactive :display => "Inactive"
111
+ end
112
+
113
+ == SYNOPSIS:
114
+
115
+ Elementals are "containers" for your elements (a.k.a. Ruby symbols).
116
+ They are declared like this:
117
+
118
+ class AccountStatus
119
+ extend Elemental
120
+ member :active, :display => 'Active', :default => true
121
+ member :delinquent, :display => "Account Past Due"
122
+ member :inactive :display => "Inactive"
123
+ end
124
+
125
+ class Fruit
126
+ extend Elemental
127
+ member :orange, :position => 10
128
+ member :banana, :position => 5, :default => true
129
+ member :blueberry, :position => 15
130
+ end
131
+
132
+ Where:
133
+ * The symbol is main accessor for Elemental
134
+ * Display is what the end-user sees. Display defaults to the symbol's to_s.
135
+ * Position lets you define a display sort order (use WidgetType.sort...)
136
+ * Position defaults to Ordinal value if not given.
137
+ * Absent Position, the default sort order is the order in which
138
+ elements are declared (ordinal).
139
+ * Specifying Position does not change Ordinal value.
140
+ * An ElementNotFoundError is raised if you try an nonexistent Symbol.
141
+
142
+ Another example:
143
+
144
+ class Color
145
+ extend Elemental
146
+ member :red, :display "#FF0000"
147
+ member :green, :display "#00FF00"
148
+ member :blue, :display "#0000FF"
149
+ end
150
+
151
+ So you roll with this for a few months and decided you don't like primary colors?
152
+ Its simple to change the color constants (in display):
153
+
154
+ class Color
155
+ extend Elemental
156
+ member :red, :display "#AA5555"
157
+ member :green, :display "#55AA55"
158
+ member :blue, :display "#5555AA"
159
+ end
160
+
161
+ Your code logic remains the same because the symbols didn't change!
162
+
163
+ Once you have your Elemental classes defined, replace your conditionals
164
+ that use string literals.
165
+
166
+ So, instead of:
167
+ if widget.widget_type == "Foo bar"
168
+
169
+ (where widget is an Activerecord instance and widget_type is an attribute/column)
170
+ (mental note: was that "FOO BAR", "foobar", "Foo Bar", "foo bar", or "Foo bar"??)
171
+
172
+ Do this and be confident:
173
+ if WidgetType[widget.widget_type] == WidgetType::foo_bar
174
+
175
+ Or shorter:
176
+ if WidgetType[widget.widget_type].is?(WidgetType::foo_bar)
177
+
178
+ Or shorter, still:
179
+ if WidgetType[widget.widget_type].is?(:foo_bar)
180
+
181
+ Although Elemental wasn't specifically written for Rails, it is trivial to put to use. Instead
182
+ if creating a new model and migration script and all that, simply establish your Elemental
183
+ class definition and then iterate the Elemental to construct your views as appropriate.
184
+
185
+ With Elemental, simply populate select dropdowns like this:
186
+
187
+ in your $RAILS_ROOT/config/initializers/constants.rb:
188
+
189
+ class AccountStatus
190
+ extend Elemental
191
+ member :active, :display => 'Active', :default => true
192
+ member :delinquent, :display => "Account Past Due"
193
+ member :inactive :display => "Inactive"
194
+ end
195
+
196
+ Then in your view:
197
+
198
+ <p><label for="account_status">Account Status</label><br/>
199
+ <%= select "user", "status",
200
+ AccountStatus.sort.map{|a| [a.display, a.value]},
201
+ { :include_blank => false,
202
+ :selected => AccountStatus.defaults.first.value }
203
+ %></select>
204
+ </p>
205
+
206
+ Or render checkboxes with:
207
+
208
+ <% Color.sort.each |c| do %>
209
+ <%= check_box_tag("user[favorite_colors][#{c.value}]",
210
+ "1", Color.defaults.detect{|color| color == c}) %>
211
+ <%= "#{c.display}"%><br />
212
+ <% end %>
213
+
214
+ Life is simplified because:
215
+ - No more migrate scripts for tiny tables
216
+ - No worries that the auto-incrementer is guaranteeing to assign
217
+ same ID accross deployments.
218
+ - No database penalties looking up rarely changing values
219
+ - No worries that an empty lookup table breaks your application logic/flow
220
+ - Values that mean something in your app are symbolic while what's displayed
221
+ and sorted on are free to change as the application grows and evolves.
222
+
223
+ More than one default is supported (dropdowns only use one, so first.value
224
+ gets you first one, radio buttons or checkboxes can use all defaults). Ordinal
225
+ position is preserved and traditionally shouldn't be changed during the life
226
+ of the project, although, Ruby being Ruby and developers saying "Ruby ain't C,"
227
+ an Elemental's elements almost definitely will get changed by some developer at
228
+ some time.
229
+
230
+ The chances of the name of the symbol changing is much lower than a developer's
231
+ tendency to keep an orderly house by sorting member elements alphabetically. As
232
+ such, the default behavior for "value" is to return the symbol rather than
233
+ ordinal value and storing as a string in DB, which seems the safest route to
234
+ take albeit not the optimal performance-wise (finding records by integral value
235
+ is faster than string searches, even with indexes in place). If you want to
236
+ override this, simply call "persist_ordinally" when you declare your Elemental
237
+ classes.
238
+
239
+ class Color
240
+ extend Elemental
241
+ persist_ordinally
242
+ member :red
243
+ member :green
244
+ member :blue
245
+ end
246
+
247
+ With that, Color::red.value returns Fixnum 0 instead of the Symbol :red
248
+
249
+ You can also associate many values with a particular element through custom accessors.
250
+ To extend the Color example further:
251
+
252
+ class Color
253
+ extend Elemental
254
+ persist_ordinally
255
+ member :red, :display => "Primary Red", :red => 255, :green => 0, :blue => 0, :hex => "#FF0000"
256
+ member :green, :display => "Primary Green", :red => 0, :green => 255, :blue => 0, :hex => "#00FF00"
257
+ member :blue, :display => "Primary Blue", :red => 0, :green => 0, :blue => 255, :hex => "#0000FF"
258
+ end
259
+
260
+ With that:
261
+ Color::red.red => 255
262
+ Color::red.green => 0
263
+ Color::red.blue => 0
264
+ Color::red.hex => "#FF0000"
265
+
266
+ Elements can be accessed multiple ways:
267
+
268
+ def test_different_retrievals_get_same_element
269
+ a1 = Car::honda
270
+ a2 = Car::Honda
271
+ a3 = Car[:honda]
272
+ a4 = Car[:Honda]
273
+ a5 = Car["Honda"]
274
+ a6 = Car["honda"]
275
+ a7 = Car.first
276
+ a8 = Car.last.succ
277
+ a9 = Car[0]
278
+ assert_element_sameness(a1, a2, :honda)
279
+ assert_element_sameness(a2, a3, :honda)
280
+ assert_element_sameness(a4, a5, :honda)
281
+ assert_element_sameness(a6, a7, :honda)
282
+ assert_element_sameness(a8, a1, :honda)
283
+ assert_element_sameness(a9, a2, :honda)
284
+ end
285
+
286
+ There are also several convenience aliases to pull ordinal, value, symbol and display:
287
+
288
+ def test_aliases
289
+ assert_equal(Car::toyota.index, Car::toyota.to_i)
290
+ assert_equal(Car::toyota.to_i, Car::toyota.to_i)
291
+ assert_equal(Car::toyota.to_int, Car::toyota.to_i)
292
+ assert_equal(Car::toyota.ord, Car::toyota.to_i)
293
+ assert_equal(Car::toyota.ordinal, Car::toyota.to_i)
294
+ assert_equal(Car::toyota.to_sym, Car::toyota.value)
295
+ assert_equal(:toyota, Car::toyota.to_sym)
296
+ assert_equal(:toyota, Car::toyota.value)
297
+ assert_equal("toyota", Car::toyota.display)
298
+ assert_equal("toyota", Car::toyota.humanize)
299
+ end
300
+
301
+ Full test coverage is provided.
302
+
303
+ One known limitation (which is probably a reflection of my not-so-great Ruby skills):
304
+
305
+ You cannot inherit and Elemental from another Elemental, esp. since none of the classes or
306
+ modules are ever actually instantiated. The following will NOT WORK:
307
+
308
+ class Fruit # OK
309
+ extend Elemental
310
+ member :orange
311
+ member :banana
312
+ member :blueberry
313
+ end
314
+
315
+ class Vegetable # OK
316
+ extend Elemental
317
+ member :potato
318
+ member :carrot
319
+ member :tomato
320
+ end
321
+
322
+ class Food # NOT OK
323
+ extend Fruit
324
+ extend Vegetable
325
+ end
326
+
327
+ Also, this doesn't work, either:
328
+
329
+ class Food < Fruit # NOT OK
330
+ extend Vegetable
331
+ end
332
+
333
+ Nor:
334
+
335
+ class ExoticFruit < Fruit # NOT OK
336
+ member :papaya
337
+ member :mango
338
+ end
339
+
340
+
341
+ == LICENSE:
342
+
343
+ (The MIT License)
344
+
345
+ Copyright (c) 2009 Michael Lang
346
+
347
+ Permission is hereby granted, free of charge, to any person obtaining
348
+ a copy of this software and associated documentation files (the
349
+ 'Software'), to deal in the Software without restriction, including
350
+ without limitation the rights to use, copy, modify, merge, publish,
351
+ distribute, sublicense, and/or sell copies of the Software, and to
352
+ permit persons to whom the Software is furnished to do so, subject to
353
+ the following conditions:
354
+
355
+ The above copyright notice and this permission notice shall be
356
+ included in all copies or substantial portions of the Software.
357
+
358
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
359
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
360
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
361
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
362
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
363
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
364
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,29 @@
1
+ # Look in the tasks/setup.rb file for the various options that can be
2
+ # configured in this Rakefile. The .rake files in the tasks directory
3
+ # are where the options are used.
4
+
5
+ begin
6
+ require 'bones'
7
+ Bones.setup
8
+ rescue LoadError
9
+ begin
10
+ load 'tasks/setup.rb'
11
+ rescue LoadError
12
+ raise RuntimeError, '### please install the "bones" gem ###'
13
+ end
14
+ end
15
+
16
+ ensure_in_path 'lib'
17
+ require 'elemental'
18
+
19
+ task :default => 'test:run'
20
+
21
+ PROJ.name = 'elemental'
22
+ PROJ.authors = 'Michael Lang'
23
+ PROJ.email = 'mwlang@cybrains.net'
24
+ PROJ.url = 'http://github.com/mwlang/elemental/tree/master'
25
+ PROJ.version = Elemental::VERSION
26
+ PROJ.rubyforge.name = 'elemental'
27
+ PROJ.spec.opts << '--color'
28
+
29
+ # EOF
@@ -0,0 +1,23 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = "elemental"
3
+ s.version = "0.1.1"
4
+ s.date = "2009-06-03"
5
+ s.summary = "Implements a set of elements that are enumerable."
6
+ s.email = "mwlang@cybrains.net"
7
+ s.homepage = "http://github.com/mwlang/elemental"
8
+ s.description = "Elemental provides enumerated set of elements allowing your code to think symbolically and unambiguously while giving you the means to easily display what end-users need to see."
9
+ s.has_rdoc = true
10
+ s.authors = ["Michael Lang"]
11
+ s.platform = Gem::Platform::RUBY
12
+ s.files = [
13
+ "elemental.gemspec",
14
+ "README.txt",
15
+ "Rakefile",
16
+ "lib/element.rb",
17
+ "lib/elemental.rb",
18
+ "test/test_elemental.rb"
19
+ ]
20
+ s.rdoc_options = ["--line-numbers", "--inline-source", "--title", "Elemental", "--main", "README.txt"]
21
+ s.extra_rdoc_files = ["README.txt"]
22
+ end
23
+
@@ -0,0 +1,106 @@
1
+ =begin
2
+ Copyright (c) 2009 Michael Lang
3
+
4
+ Permission is hereby granted, free of charge, to any person obtaining
5
+ a copy of this software and associated documentation files (the
6
+ 'Software'), to deal in the Software without restriction, including
7
+ without limitation the rights to use, copy, modify, merge, publish,
8
+ distribute, sublicense, and/or sell copies of the Software, and to
9
+ permit persons to whom the Software is furnished to do so, subject to
10
+ the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be
13
+ included in all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
16
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
18
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
19
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
20
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
21
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+ =end
23
+ class Element
24
+
25
+ attr_accessor :symbol, :ordinal, :display, :position, :default
26
+
27
+ # Initializes a new element
28
+ def initialize(elemental, symbol, ordinal, options={})
29
+ @elemental = elemental
30
+ @symbol = symbol
31
+ @ordinal = ordinal
32
+
33
+ # Sets all options and optionally creates an accessor method
34
+ options.each do |k, v|
35
+ eval("def #{k}; @#{k}; end") unless respond_to?(k)
36
+ v.is_a?(String) ? eval("@#{k} = '#{v}'") : eval("@#{k} = #{v}")
37
+ end
38
+
39
+ # Force the following values (overrides above looped assignments)
40
+ @default = options[:default] || false
41
+ @display = options[:display] || symbol.to_s
42
+ @position = options[:position] || ordinal
43
+ end
44
+
45
+ def is?(value)
46
+ self == (value.is_a?(Element) ? value : @elemental[value])
47
+ end
48
+
49
+ def value
50
+ @elemental.value_as_ordinal ? to_i : to_sym
51
+ end
52
+
53
+ # Will sort on position (which defaults to ordinal if not explicitly set)
54
+ def <=>(other)
55
+ position <=> other.position
56
+ end
57
+
58
+ # Returns the element that follows this element.
59
+ # If this is last element, returns first element
60
+ def succ
61
+ @elemental.succ(@symbol)
62
+ end
63
+
64
+ # Returns the element that precedes this element.
65
+ # If this is the first element, returns last element
66
+ def pred
67
+ @elemental.pred(@symbol)
68
+ end
69
+
70
+ # Returns the symbol for this element as a string
71
+ def to_s
72
+ @symbol.to_s
73
+ end
74
+
75
+ # Returns this element's symbol
76
+ def to_sym
77
+ @symbol
78
+ end
79
+
80
+ # Returns the ordinal value for this element
81
+ def index
82
+ @ordinal
83
+ end
84
+
85
+ # Returns true if is default. (useful if populating select lists)
86
+ def default?
87
+ @default
88
+ end
89
+
90
+ # Generates a reasonable inspect string
91
+ def inspect
92
+ "#<#{self.class}:#{self.object_id} {symbol => :#{@symbol}, " +
93
+ "ordinal => #{@ordinal}, display => \"#{@display}\", " +
94
+ "position => #{@position}, default => #{@default}}>"
95
+ end
96
+
97
+ # swiped from Rails...
98
+ def humanize
99
+ return @display if @display != to_s
100
+ return self.to_s.gsub(/_id$/, "").gsub(/_/, " ").capitalize
101
+ end
102
+
103
+ alias :to_i :index
104
+ alias :to_int :index
105
+ alias :ord :index
106
+ end
@@ -0,0 +1,169 @@
1
+ =begin
2
+ Copyright (c) 2009 Michael Lang
3
+
4
+ Permission is hereby granted, free of charge, to any person obtaining
5
+ a copy of this software and associated documentation files (the
6
+ 'Software'), to deal in the Software without restriction, including
7
+ without limitation the rights to use, copy, modify, merge, publish,
8
+ distribute, sublicense, and/or sell copies of the Software, and to
9
+ permit persons to whom the Software is furnished to do so, subject to
10
+ the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be
13
+ included in all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
16
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
18
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
19
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
20
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
21
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+ =end
23
+
24
+ begin
25
+ require 'lib/element'
26
+ rescue LoadError
27
+ require 'element'
28
+ end
29
+
30
+ module Elemental
31
+
32
+ VERSION = '0.1.1'
33
+
34
+ include Enumerable
35
+
36
+ attr_reader :value_as_ordinal
37
+
38
+ # Causes the Element#value method to return ordinal value, which
39
+ # normally returns the ruby symbol
40
+ #
41
+ # Note: If you choose for Element#value to return ordinal and you are
42
+ # storing value to database, then you should always APPEND new elements
43
+ # to the declared Elemental class. Otherwise, you WILL CHANGE the
44
+ # ordinal values and thus invalidate any persistent stores. If you're
45
+ # not persistently storing ordinal values, then order of membership is moot.
46
+ def persist_ordinally
47
+ @value_as_ordinal = true
48
+ end
49
+
50
+ # Adds an element in the order given to the enumerable's class
51
+ # Order matters if you store ordinal to database or other persistent stores
52
+ def member(symbol, options={})
53
+ @unordered_elements ||= {}
54
+ @ordered_elements ||= []
55
+ symbol = conform_to_symbol(symbol)
56
+ element = Element.new(self, symbol, @ordered_elements.size, options)
57
+
58
+ @unordered_elements[symbol] = element
59
+ @ordered_elements << element
60
+ end
61
+
62
+ # allows you to define aliases for a given member with adding
63
+ # additional elements to the elemental class.
64
+ #
65
+ # class Fruit
66
+ # extend Elemental
67
+ # member :apple
68
+ # member :kiwi
69
+ # synonym :machintosh, :apple
70
+ # end
71
+ #
72
+ # will yield an Elemental with only two elements (apple and kiwi),
73
+ # but give the ability to retrieve Fruit::apple using Fruit::machintosh
74
+ def synonym(new_symbol, existing_symbol)
75
+ element = self[existing_symbol]
76
+ @unordered_elements[conform_to_symbol(new_symbol)] = element
77
+ end
78
+
79
+ # Returns the first element in this class
80
+ def first
81
+ @ordered_elements.first
82
+ end
83
+
84
+ # Returns the last element in this class
85
+ def last
86
+ @ordered_elements.last
87
+ end
88
+
89
+ # Returns the number of elements added
90
+ def size
91
+ @ordered_elements.size
92
+ end
93
+
94
+ # Returns the element that follows the given element. If given element is
95
+ # last element, then first element is returned.
96
+ def succ(value)
97
+ index = self[value].to_i + 1
98
+ (index >= size) ? first : @ordered_elements[index]
99
+ end
100
+
101
+ # Returns the element that precedes the given element. If given element is
102
+ # the first element, then last element is returned.
103
+ def pred(value)
104
+ index = self[value].to_i - 1
105
+ (index < 0) ? last : @ordered_elements[index]
106
+ end
107
+
108
+ # Shortcut to getting to a specific element
109
+ # Can get by symbol or constant (i.e. Color[:red] or Color[:Red])
110
+ def [](value)
111
+ value.is_a?(Fixnum) ? get_ordered_element(value) : get_unordered_element(value)
112
+ end
113
+
114
+ # iterates over the elements, passing each to the given block
115
+ def each(&block)
116
+ @ordered_elements.each(&block)
117
+ end
118
+
119
+ # Allows Car::honda
120
+ def method_missing(value)
121
+ self[value]
122
+ end
123
+
124
+ # Allows Car::Honda
125
+ def const_missing(const)
126
+ get_unordered_element(const)
127
+ end
128
+
129
+ # Returns all elements that are flagged as a default
130
+ def defaults
131
+ @ordered_elements.select{|s| s.default?}
132
+ end
133
+
134
+ # Outputs a sensible inspect string
135
+ def inspect
136
+ "#<#{self}:#{object_id} #{@ordered_elements.inspect}>"
137
+ end
138
+
139
+ private
140
+
141
+ # Takes a "CamelCased-String" and returns "camel_cased_string"
142
+ def underscore(camel_cased_word)
143
+ camel_cased_word.to_s.
144
+ gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
145
+ gsub(/([a-z\d])([A-Z])/,'\1_\2').
146
+ tr("-", "_").
147
+ downcase
148
+ end
149
+
150
+ # Transforms given string or symbol
151
+ # into a :lowered_case_underscored_symbol
152
+ def conform_to_symbol(text_or_symbol)
153
+ underscore(text_or_symbol.to_s).downcase.to_sym
154
+ end
155
+
156
+ # returns the nth element, raising an exception if index out of bounds
157
+ def get_ordered_element(index)
158
+ result = @ordered_elements[index]
159
+ raise RuntimeError if result.nil?
160
+ result
161
+ end
162
+
163
+ # returns the requested element by name, raising exception if non-existent
164
+ def get_unordered_element(what)
165
+ result = @unordered_elements[conform_to_symbol(what)]
166
+ raise RuntimeError if result.nil?
167
+ result
168
+ end
169
+ end
@@ -0,0 +1,399 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'test/unit'
4
+ require 'lib/elemental'
5
+
6
+ module TestElemental
7
+
8
+ class TestElemental < Test::Unit::TestCase
9
+
10
+ class Fruit
11
+ extend Elemental
12
+ member :apple
13
+ member :pear, :default => true
14
+ member :banana, :default => true
15
+ member :kiwi
16
+
17
+ synonym :machintosh, :apple
18
+ end
19
+
20
+ FRUIT_SIZE = 4
21
+
22
+ class Color
23
+ extend Elemental
24
+ member :blue, :display => "Hazel Blue", :default => true
25
+ member :red, :display => "Fire Engine Red"
26
+ member :yellow
27
+ end
28
+
29
+ COLOR_SIZE = 3
30
+
31
+ class Car
32
+ extend Elemental
33
+ member :honda, :position => 100
34
+ member :toyota, :position => 30
35
+ member :ford, :position => 50
36
+ member :gm, :position => 25
37
+ member :mazda, :position => 1
38
+ end
39
+
40
+ CAR_SIZE = 5
41
+
42
+ class JumbledMess
43
+ extend Elemental
44
+ member :testing
45
+ member :Oh_one
46
+ member :two_three
47
+ member :FourFiveSix
48
+ member :what_an_id
49
+ member :your_idea
50
+ end
51
+
52
+ class TalkingNumbers
53
+ extend Elemental
54
+ persist_ordinally
55
+ member :zilch, :display => "zero", :alt => "zip", :zero => true, :custom => 0
56
+ member :uno,:display => "one", :alt => "1st", :zero => false, :custom => 1.0
57
+ member :dos, :display => "two", :alt => "2nd", :zero => false, :custom => 2
58
+ member :tres, :display => "three", :alt => "3rd", :zero => false, :custom => "3"
59
+ end
60
+
61
+ def test_values_as_ordinal
62
+ one = TalkingNumbers[:uno]
63
+ two = TalkingNumbers["dos"]
64
+ three = TalkingNumbers[3]
65
+
66
+ assert_equal(one.to_i, one.value)
67
+ assert_equal(2, two.value)
68
+ assert_equal(3, three.value)
69
+ end
70
+
71
+ def test_humanize
72
+ assert_equal("Testing", JumbledMess::testing.humanize)
73
+ assert_equal("Oh one", JumbledMess::oh_one.humanize)
74
+ assert_equal("Two three", JumbledMess::two_three.humanize)
75
+ assert_equal("Four five six", JumbledMess::FourFiveSix.humanize)
76
+ end
77
+
78
+ # Test the order of breadth for each
79
+ def test_size_of_elemental
80
+ assert_equal(FRUIT_SIZE, Fruit.size)
81
+ assert_equal(COLOR_SIZE, Color.size)
82
+ assert_equal(CAR_SIZE, Car.size)
83
+ end
84
+
85
+ def assert_can_map_elements
86
+ assert_not_nil(Car.collect{|c| c.value})
87
+ end
88
+
89
+ def assert_element_sameness(a, b, symbol)
90
+ assert_not_nil(a)
91
+ assert_not_nil(b)
92
+ assert(a.is_a?(Element))
93
+ assert(b.is_a?(Element))
94
+ assert_equal(a, b)
95
+ assert_equal(symbol, a.to_sym)
96
+ assert_equal(symbol, b.to_sym)
97
+ assert_equal(a, b)
98
+ end
99
+
100
+ def test_basic_retrieval
101
+ assert_not_nil(Fruit::apple)
102
+ assert_not_nil(Fruit::banana)
103
+ assert_not_nil(Fruit::pear)
104
+ assert_not_nil(Fruit::kiwi)
105
+ end
106
+
107
+ def test_jumbled_retrival
108
+ assert_not_nil(JumbledMess::testing)
109
+ assert_not_nil(JumbledMess::two_three)
110
+ assert_not_nil(JumbledMess::what_an_id)
111
+ assert_not_nil(JumbledMess::your_idea)
112
+ assert_not_nil(JumbledMess::oh_one)
113
+ assert_not_nil(JumbledMess["oh_one"])
114
+
115
+ assert_not_nil(JumbledMess::Oh_one)
116
+ assert_not_nil(JumbledMess::Oh_One)
117
+ assert_not_nil(JumbledMess::FourFiveSix)
118
+ assert_not_nil(JumbledMess::Four_Five_six)
119
+ end
120
+
121
+ def test_basic_nonretrieval
122
+ assert_raise(RuntimeError) { Fruit::tomato }
123
+ assert_raise(RuntimeError) { Fruit::Potato }
124
+ assert_raise(RuntimeError) { Fruit[:squash] }
125
+ assert_raise(RuntimeError) { Fruit["okra"] }
126
+ assert_raise(RuntimeError) { Fruit[10] }
127
+ end
128
+
129
+ def test_retrieval_by_ordinal
130
+ apple1 = Fruit[0]
131
+ apple2 = Fruit.select{|s| s.to_sym == :apple}.first
132
+ assert_element_sameness(apple1, apple2, :apple)
133
+
134
+ pear1 = Fruit[1]
135
+ pear2 = Fruit[:pear]
136
+ assert_element_sameness(pear1, pear2, :pear)
137
+
138
+ kiwi1 = Fruit[-1]
139
+ kiwi2 = Fruit.last
140
+ assert_element_sameness(kiwi1, kiwi2, :kiwi)
141
+ end
142
+
143
+ def test_retrieval_by_symbol
144
+ a1 = Fruit::apple
145
+ a2 = Fruit[:apple]
146
+ a3 = Fruit.select{|s| s.to_sym == :apple}.first
147
+ assert_element_sameness(a1, a2, :apple)
148
+ assert_element_sameness(a1, a3, :apple)
149
+ assert_element_sameness(a2, a3, :apple)
150
+
151
+ b1 = Fruit::banana
152
+ b2 = Fruit[:banana]
153
+ b3 = Fruit.select{|s| s.to_sym == :banana}.first
154
+ assert_element_sameness(b1, b2, :banana)
155
+ assert_element_sameness(b1, b3, :banana)
156
+ assert_element_sameness(b2, b3, :banana)
157
+ end
158
+
159
+ def test_retrieval_by_constant
160
+ a1 = Car::Toyota
161
+ a2 = Car[:Toyota]
162
+ a3 = Car.select{|s| s.to_sym == :toyota}.first
163
+ assert_element_sameness(a1, a2, :toyota)
164
+ assert_element_sameness(a1, a3, :toyota)
165
+ assert_element_sameness(a2, a3, :toyota)
166
+ end
167
+
168
+ def test_different_retrievals_get_same_element
169
+ a1 = Car::honda
170
+ a2 = Car::Honda
171
+ a3 = Car[:honda]
172
+ a4 = Car[:Honda]
173
+ a5 = Car["Honda"]
174
+ a6 = Car["honda"]
175
+ a7 = Car.first
176
+ a8 = Car.last.succ
177
+ a9 = Car[0]
178
+ assert_element_sameness(a1, a2, :honda)
179
+ assert_element_sameness(a2, a3, :honda)
180
+ assert_element_sameness(a4, a5, :honda)
181
+ assert_element_sameness(a6, a7, :honda)
182
+ assert_element_sameness(a8, a1, :honda)
183
+ assert_element_sameness(a9, a2, :honda)
184
+ end
185
+
186
+ def test_get_first_element
187
+ e1 = Color.first
188
+ e2 = Color::blue
189
+ assert_element_sameness(e1,e2,:blue)
190
+ assert_equal(e1.to_i, 0)
191
+ assert_equal(e1.to_i, e2.ord)
192
+ assert_equal(e2.ordinal, 0)
193
+ end
194
+
195
+ def test_walk_succ
196
+ e1 = Color.first
197
+ assert_element_sameness(e1, Color::blue, :blue)
198
+ assert_equal(0, e1.ordinal)
199
+ e1 = e1.succ
200
+ assert_element_sameness(e1, Color::red, :red)
201
+ assert_equal(1, e1.ordinal)
202
+ e1 = e1.succ
203
+ assert_element_sameness(e1, Color::yellow, :yellow)
204
+ assert_equal(2, e1.ordinal)
205
+ e1 = e1.succ
206
+ assert_element_sameness(e1, Color::blue, :blue)
207
+ assert_equal(0, e1.ordinal)
208
+ end
209
+
210
+ def test_get_last_element
211
+ e1 = Color.last
212
+ e2 = Color::yellow
213
+ assert_element_sameness(e1,e2,:yellow)
214
+ assert_equal(e1.to_i, COLOR_SIZE - 1)
215
+ assert_equal(e1.to_i, e2.ord)
216
+ assert_equal(e2.ordinal, COLOR_SIZE - 1)
217
+ end
218
+
219
+ def test_walk_pred
220
+ e1 = Color.first
221
+ assert_element_sameness(e1, Color::blue, :blue)
222
+ assert_equal(0, e1.ordinal)
223
+
224
+ e1 = e1.pred
225
+ assert_element_sameness(e1, Color::yellow, :yellow)
226
+ assert_equal(2, e1.ordinal)
227
+
228
+ e1 = e1.pred
229
+ assert_element_sameness(e1, Color::red, :red)
230
+ assert_equal(1, e1.ordinal)
231
+
232
+ e1 = e1.pred
233
+ assert_element_sameness(e1, Color::blue, :blue)
234
+ assert_equal(0, e1.ordinal)
235
+ end
236
+
237
+ def test_get_as_array
238
+ a = Color.to_a
239
+ assert(a.is_a?(Array))
240
+ assert_equal(COLOR_SIZE, a.size)
241
+ assert(a.first.is_a?(Element))
242
+ assert_element_sameness(Color.first, a.first, :blue)
243
+ assert_element_sameness(Color.first.succ, a[1], :red)
244
+ end
245
+
246
+ def test_aliases
247
+ assert_equal(Car::toyota.index, Car::toyota.to_i)
248
+ assert_equal(Car::toyota.to_i, Car::toyota.to_i)
249
+ assert_equal(Car::toyota.to_int, Car::toyota.to_i)
250
+ assert_equal(Car::toyota.ord, Car::toyota.to_i)
251
+ assert_equal(Car::toyota.ordinal, Car::toyota.to_i)
252
+ assert_equal(Car::toyota.to_sym, Car::toyota.value)
253
+ assert_equal(:toyota, Car::toyota.to_sym)
254
+ assert_equal(:toyota, Car::toyota.value)
255
+ assert_equal("toyota", Car::toyota.display)
256
+ assert_equal("Toyota", Car::toyota.humanize)
257
+ end
258
+
259
+ def test_to_a_gives_elements_in_ordinal_position
260
+ a = Color.to_a
261
+ assert(a.is_a?(Array))
262
+ assert_equal(COLOR_SIZE, a.size)
263
+ assert(a.first.is_a?(Element))
264
+ a.each_with_index do |element, index|
265
+ assert_equal(index, element.ordinal)
266
+ end
267
+ end
268
+
269
+ def test_each_gives_elements_in_ordinal_position
270
+ Car.each_with_index do |element, index|
271
+ assert_equal(index, element.ordinal)
272
+ end
273
+ end
274
+
275
+ def test_sorted_by_position_is_equal_to_ordinal_when_position_not_specified
276
+ a = Color.to_a
277
+ b = Color.sort
278
+ assert(b.is_a?(Array))
279
+ assert_equal(COLOR_SIZE, b.size)
280
+ assert(b.first.is_a?(Element))
281
+
282
+ # Position (and thus order) should be same as ordinal order when position not specified at creation
283
+ a.each_with_index do |element, index|
284
+ assert_element_sameness(element, b[index], element.to_sym)
285
+ end
286
+
287
+ Color.sort.each_with_index do |element, index|
288
+ assert_element_sameness(element, b[index], element.to_sym)
289
+ end
290
+ end
291
+
292
+ def test_can_use_map
293
+ Car.map{|m| assert(m.is_a?(Element))}
294
+ end
295
+
296
+ def test_equality
297
+ assert_equal(:mazda, Car::mazda.value)
298
+ assert_equal(:mazda, Car::mazda.to_sym)
299
+ end
300
+
301
+ def test_sorted_by_position_is_different_than_ordinal_order
302
+ a = Car.to_a
303
+ b = Car.sort
304
+ assert(b.is_a?(Array))
305
+ assert_equal(CAR_SIZE, b.size)
306
+ assert(b.first.is_a?(Element))
307
+ assert_element_sameness(Car::mazda, b.first, :mazda)
308
+ assert_element_sameness(Car::honda, b.last, :honda)
309
+
310
+ # Position (and thus order) should be same as ordinal order when position not specified at creation
311
+ a.each_with_index do |element, index|
312
+ assert_not_equal(b[index], element)
313
+ end
314
+
315
+ def test_sorted_by_position_is_ascending_by_position
316
+ b = Car.sort
317
+ last_ordinal = -1
318
+ b.each_with_index do |element, index|
319
+ assert(last_ordinal < element.ordinal)
320
+ last_ordinal = element.ordinal
321
+ end
322
+ end
323
+ end
324
+
325
+ def test_display_can_be_set
326
+ assert_equal("yellow", Color::yellow.to_s)
327
+ assert_equal("yellow", "#{Color::yellow}")
328
+ assert_equal("yellow", Color::yellow.display)
329
+
330
+ a = Color::blue
331
+ assert_equal("Hazel Blue", a.display)
332
+ assert_equal("blue", "#{a}")
333
+
334
+ assert_equal("Fire Engine Red", Color::red.display)
335
+ end
336
+
337
+ def test_setting_display_does_not_affect_symbol_to_string
338
+ a = Color::blue
339
+ assert_equal("Hazel Blue", a.display)
340
+ assert_equal("blue", "#{a}")
341
+ assert_not_equal("Hazel Blue", a.to_s)
342
+ end
343
+
344
+ def test_default_is_false_by_default
345
+ Car.each{|c| assert_equal(false, c.default?)}
346
+ end
347
+
348
+ def test_banana_is_default
349
+ assert_equal(true, Fruit::banana.default?)
350
+ end
351
+
352
+ def test_can_get_array_of_defaults
353
+ a = Fruit.defaults
354
+ b = [Fruit::banana, Fruit::pear]
355
+ assert_equal(2, a.size)
356
+ assert_equal(2, b.size)
357
+ b.each{|element| assert(a.include?(element), "a, #{a.inspect} does not contain #{element.inspect}")}
358
+ end
359
+
360
+ def test_defaults_is_only_defaults
361
+ a = [Fruit::banana, Fruit::pear]
362
+ c = Fruit.reject{|r| a.include?(r)}
363
+ assert_equal(2, a.size)
364
+ assert_equal(FRUIT_SIZE - 2, c.size)
365
+ c.each{|element| assert(!a.include?(element), "a, #{a.inspect} should not contain #{element.inspect}")}
366
+ end
367
+
368
+ def test_is_conditional
369
+ assert_equal(true, Fruit::banana.is?(:banana))
370
+ assert_equal(true, Fruit::banana.is?(Fruit::banana))
371
+ assert_equal(false, Fruit::banana.is?(:kiwi))
372
+ assert_equal(true, TalkingNumbers[1].is?(:uno))
373
+ assert_equal(true, TalkingNumbers[2].is?("dos"))
374
+ assert_equal(true, TalkingNumbers["tres"].is?(3))
375
+ end
376
+
377
+ def test_extended_attributes
378
+ assert_equal('1st', TalkingNumbers[1].alt)
379
+ assert_equal('2nd', TalkingNumbers[:dos].alt)
380
+ assert_equal('3rd', TalkingNumbers["tres"].alt)
381
+
382
+ assert_equal(true, TalkingNumbers[0].zero)
383
+ assert_equal(false, TalkingNumbers[1].zero)
384
+ assert_equal(false, TalkingNumbers[:dos].zero)
385
+ assert_equal(false, TalkingNumbers["tres"].zero)
386
+
387
+ assert_equal(0, TalkingNumbers[0].custom)
388
+ assert_equal(true, TalkingNumbers[1].custom.is_a?(Float))
389
+ assert_equal(true, TalkingNumbers[2].custom.is_a?(Fixnum))
390
+ assert_equal(true, TalkingNumbers[3].custom.is_a?(String))
391
+ end
392
+
393
+ def test_synonym_accessor
394
+ a = Fruit::apple
395
+ b = Fruit::machintosh
396
+ assert_equal(a, b)
397
+ end
398
+ end
399
+ end
metadata ADDED
@@ -0,0 +1,63 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: elemental
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Michael Lang
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-06-03 00:00:00 -04:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description: Elemental provides enumerated set of elements allowing your code to think symbolically and unambiguously while giving you the means to easily display what end-users need to see.
17
+ email: mwlang@cybrains.net
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files:
23
+ - README.txt
24
+ files:
25
+ - elemental.gemspec
26
+ - README.txt
27
+ - Rakefile
28
+ - lib/element.rb
29
+ - lib/elemental.rb
30
+ - test/test_elemental.rb
31
+ has_rdoc: true
32
+ homepage: http://github.com/mwlang/elemental
33
+ post_install_message:
34
+ rdoc_options:
35
+ - --line-numbers
36
+ - --inline-source
37
+ - --title
38
+ - Elemental
39
+ - --main
40
+ - README.txt
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.3.1
59
+ signing_key:
60
+ specification_version: 2
61
+ summary: Implements a set of elements that are enumerable.
62
+ test_files: []
63
+