elemental 0.1.1 → 0.1.2

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