elemental 0.1.1

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.
@@ -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
+