elemental 0.1.1 → 0.1.2

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