valuable 0.9.5 → 0.9.6

Sign up to get free protection for your applications and to get access to all the features.
data/README.markdown CHANGED
@@ -7,24 +7,199 @@ Valuable provides DRY decoration like attr_accessor, but includes default values
7
7
 
8
8
  Tested with [Rubinius](http://www.rubini.us "Rubinius"), 1.8.7, 1.9.1, 1.9.2, 1.9.3
9
9
 
10
+ Version 0.9.x is considered stable.
11
+
12
+ Valuable was originally created to avoid the repetition of writing the constructor-accepts-a-hash method. It has evolved, but at its core are still the same concepts.
13
+
14
+ Contents
15
+ --------
16
+
17
+ - [Frequent Uses](#frequent-uses)
18
+ - [Methods](#methods) ( [Class-Level](#class-level-methods), [Instance-Level](#instance-level-methods) )
19
+ - [Installation](#installation)
20
+ - [Usage & Examples](#usage--examples)
21
+ - [Constructor Accepts an Attributes Hash](#constructor-accepts-an-attributes-hash)
22
+ - [Default Values](#default-values)
23
+ - [Nil Values](#nil-values)
24
+ - [Aliases](#aliases)
25
+ - [Formatting Input](#formatting-input)
26
+ - [Pre-Defined Formatters](#pre-defined-formatters)
27
+ - [Collections](#collections)
28
+ - [Formatting Collections](#formatting-collections)
29
+ - [Registering Formatters](#registering-formatters)
30
+ - [More about Attributesd](#more-about-attributesd)
31
+ - [Advanced Input Parsing](#advanced-input-parsing)
32
+ - [Advanced Defaults](#advanced-defaults)
33
+ - [Advanced Collection Formatting](#advanced-collection-formatting)
34
+
10
35
  Frequent Uses
11
- -------------
36
+ =============
37
+
38
+ Valuable was created to help you quickly model things. Things I find myself modeling:
39
+
40
+ + **data imported from JSON, XML, etc**
41
+ + **the result of an API call**
42
+ + **a subset of some data in an ORM class** say you have a class Person with street, city, state and zip. It might not make sense to store this in a separate table, but you can still create an Address model to hold address-related logic and state like geocode, post_office_box? and Address#==
43
+ + **as a presenter that wraps model** This way you keep view-specific methods out of views and models.
44
+ + **as a presenter that aggregates several models** Generating a map might involve coordinating several different collections of data. Create a valuable class to handle that integration.
45
+ + **to model search forms** - Use Valuable to model an advanced search form. Create an attribute for each drop-down, check-boxe, and text field, and constants to store options. Integrates easily with Rails via @search = CustomerSearch.new(params[:search]) and form_for(@search, :url => ...)
46
+ + **to model reports** like search forms, reports can be stateful when they have critiera that can be selected via form.
47
+ + **as a query builder** ie, "I need to create an (Arel or SQL) query based off of form input." (see previous two points)
48
+ + **experiments / spikes**
49
+ + **factories** factories need well-defined input, so valuable is a great fit.
50
+
51
+ Methods
52
+ =============
53
+
54
+ ###Class-Level Methods
55
+
56
+ Use these methods to define and inspect provides the following methods:
57
+
58
+ **has_value( field_name, attributes = {})**
59
+
60
+ creates a getter and setter named field_name
61
+ options:
62
+ :default - provide a default value
63
+ has_value :status, :default => 'Active'
64
+ >> Task.new.status
65
+ => 'Active'
66
+
67
+ :alias - create setters and getters with the name of the attribute and _also_ with the alias.
68
+
69
+ see [Aliases](#aliases) for more information.
70
+
71
+ :klass - pre-format the input with one of the [predefined formatters](#pre-defined-formatters), as a class, or with your custom formatter.
72
+ has_value :age, :klass => :integer
73
+ >> Person.new(:age => '15').age.class
74
+ => Fixnum
75
+
76
+ has_value :phone_number, :klass => PhoneNumber
77
+ >> jenny = Person.new(:phone_number => '2018675309')
78
+
79
+ >> jenny.phone_number == PhoneNumber.new('2018675309')
80
+ => true
81
+
82
+ see [Formatting Input](#formatting-input) for more information.
83
+
84
+ :parse_with - Sometimes you want to instanciate with a method other than new... one example being Date.parse
85
+ has_value :dob, :klass => Date, :parse_with => :parse
86
+
87
+ **has_collection( field_name, attributes = {} )**
88
+
89
+ like has_value, this creates a getter and setter. The default value is an array.
90
+
91
+ class Person
92
+ has_collection :friends
93
+ end
94
+
95
+ >> Person.new.friends
96
+ => []
97
+
98
+ **attributes**
99
+
100
+ an array of attributes you have defined on a model.
101
+
102
+ class Person < Valuable
103
+ has_value :first_name
104
+ has_value :last_name
105
+ end
106
+
107
+ >> Person.attributes
108
+ => [:first_name, :last_name]
109
+
110
+ **defaults**
111
+
112
+ A hash of the attributes with their default values. Attributes defined without default values do not appear in this list.
113
+
114
+ class Pastry < Valuable
115
+ has_value :primary_ingredient, :default => :sugar
116
+ has_value :att_with_no_default
117
+ end
118
+
119
+ >> Pastry.defaults
120
+ => {:primary_ingredient => :sugar}
121
+
122
+ **register_formatter(name, &block)**
123
+
124
+ allows you to provide custom code to pre-format attributes, if the included ones are not sufficient. For instance,
125
+ you might wish to register an 'orientation' formatter that accepts either angles or 'N', 'S', 'E', 'W', and converts
126
+ those to angles.
127
+
128
+ NOTE: as with other formatters, nil values will not be passed to the formatter. The attribute will simply be set to nil. If this is an issue, let me know.
129
+
130
+ **acts_as_permissive**
131
+
132
+ Valuable classes typically raise an error if you instantiate them with attributes that have not been predefined.
133
+ This method makes valuable ignore any unknown attributes.
134
+
135
+ ###Instance-Level Methods
136
+
137
+ **attributes**
138
+
139
+ provides a hash of the attributes and their values.
140
+
141
+ class Party < Valuable
142
+ has_value :host
143
+ has_value :theme
144
+ has_value :time, :default => '6pm'
145
+ end
12
146
 
13
- **pre-refactor modeling** to model a class you want to abstract but know would be a pain... as in, "I would love to pull Appointment out of this WorkOrder class, but since that isn't going to happen soon, let me quickly create WorkOrder.appointments... I can then create Appointment\#to\_s, appointment.end_time, appointment.duration, etc. I can use that to facilitate emitting XML or doing something with views, rather than polluting WorkOrder with appointment-related logic."
147
+ >> party = Party.new(:theme => 'Black and Whitle')
14
148
 
15
- **as a presenter** as in, "I need to take in a few different models to generate this map... I need a class that models the integration, but I don't need to persist that to a database."
149
+ >> party.attributes
150
+ => {:theme => 'Black and White', :time => '6pm'}
16
151
 
17
- **creating models from non-standard data sources** to keep data from non-standard data sources in memory during an import or to render data from an API call.
152
+ # note that the 'host' attribute was not set by default, at
153
+ # instantiation, or via the setter method party.host=, so
154
+ # it does not appear in the attributes hash.
18
155
 
19
- Type Casting in Ruby? You must be crazy...
20
- ------------------------------------------
21
- Yeah, I get that alot. I mean, about type casting. I'm not writing
22
- C# over here. Rails does it, they just don't call it type casting,
23
- so no one complains when they pass in "2" as a parameter and mysteriously
24
- it ends up as an integer. In fact, I'm going to start using the euphamism
25
- 'Formatting' just so people will stop looking at me that way.
156
+ **update_attributes(atts={})**
26
157
 
27
- Say you're getting information for a directory from a web service via JSON:
158
+ Accepts a hash of :attribute => :value and updates each associated attributes.
159
+ Will raise an exception if any of the keys isn't already set up in the class, unless you call acts_as_permissive.
160
+
161
+ class Tomatoe
162
+ has_value :color
163
+ end
164
+
165
+ >> t = Tomatoe.new(:color => 'green')
166
+ >> t.color
167
+ => 'green'
168
+ >> t.update_attributes(:color => 'red')
169
+ >> t.color
170
+ => 'red'
171
+
172
+ **write_attribute(att_name, value)**
173
+
174
+ this method is called by all the setters and, obviously,
175
+ update_attributes. Using a formatter (if specified), it
176
+ updates the attributes hash.
177
+
178
+ class Chicken
179
+ has_value :gender
180
+ end
181
+
182
+ >> c = Chicken.new
183
+
184
+ >> c.gender
185
+ => nil
186
+
187
+ >> c.write_attribute(:gender, 'F')
188
+
189
+ >> c.gender
190
+ => 'F'
191
+
192
+ Installation
193
+ ============
194
+
195
+ if using bundler, add this to your gemfile:
196
+
197
+ gem 'valuable'
198
+
199
+ and the examples below should work.
200
+
201
+ Usage & Examples
202
+ ================
28
203
 
29
204
  class Person < Valuable
30
205
  has_value :name
@@ -32,13 +207,16 @@ Say you're getting information for a directory from a web service via JSON:
32
207
  has_value :phone_number, :klass => PhoneNumber
33
208
  # see /examples/phone_number.rb
34
209
 
35
- 'person' =>
36
- 'name' => 'Mr. Freud',
37
- 'age' => "344",
38
- 'phone_number' => '8002195642',
39
- 'specialization_code' => "2106"
40
-
41
- you'll end up with this:
210
+ params =
211
+ {
212
+ 'person' =>
213
+ {
214
+ 'name' => 'Mr. Freud',
215
+ 'age' => "344",
216
+ 'phone_number' => '8002195642',
217
+ 'specialization_code' => "2106"
218
+ }
219
+ }
42
220
 
43
221
  >> p = Person.new(params[:person])
44
222
 
@@ -52,17 +230,12 @@ you'll end up with this:
52
230
  => PhoneNumber
53
231
 
54
232
  "Yeah, I could have just done that myself."
233
+
55
234
  "Right, but now you don't have to."
56
235
 
57
- Basic Syntax
58
- ------------
59
236
 
60
- class Fruit < Valuable
61
- has_value :name
62
- has_collection :vitamins
63
- end
64
-
65
- _constructor accepts an attributes hash_
237
+ Constructor Accepts an Attributes Hash
238
+ --------------------------------------
66
239
 
67
240
  >> apple = Fruit.new(:name => 'Apple')
68
241
 
@@ -72,17 +245,10 @@ _constructor accepts an attributes hash_
72
245
  >> apple.vitamins
73
246
  => []
74
247
 
75
- _default values_
76
-
77
- Default values are used when no value is provided to the constructor. If the value nil is provided, nil will be used instead of the default. Default values are populated on instanciation.
78
-
79
- When a default value and a klass are specified, the default value will NOT be cast to type klass -- you must do it.
248
+ Default Values
249
+ --------------
80
250
 
81
- If a value having a default is set to null after it is constructed, it will NOT be set to the default.
82
-
83
- If there is no default value, the result will be nil, EVEN if type casting is provided. Thus, a field typically cast as an Integer can be nil. See calculation of average.
84
-
85
- The :default option will accept a lambda and call it on instanciation.
251
+ Default values are... um... you know.
86
252
 
87
253
  class Developer
88
254
  has_value :name
@@ -97,12 +263,45 @@ The :default option will accept a lambda and call it on instanciation.
97
263
  >> dev.nickname
98
264
  => 'mort'
99
265
 
100
- _setting a value to nil overrides the default._
266
+ If there is no default value, the result will be nil, EVEN if type casting is provided. Thus, a field typically cast as an Integer can be nil. See calculation of average example.
101
267
 
102
- >> Developer.new(:name => 'KDD', :nickname => nil).nickname
103
- => nil
268
+ See also:
269
+ + [nil values](#nil-values)
270
+ + [Advanced Defaults](#advanced-defaults)
271
+
272
+ **Note:** When a default value and a klass are specified, the default value will NOT be cast to type klass -- you must do it. Example:
273
+
274
+ class Person
275
+
276
+ # WRONG!
277
+ has_value :dob, :klass => Date, :default => '2012-07-26'
278
+
279
+ # Correct
280
+ has_value :dob, :klass => Date, :default => Date.parse('2012-07-26')
281
+
282
+ end
283
+
284
+
285
+ Nil Values
286
+ ----------
287
+
288
+ Setting an attribute to nil always results in it being nil. [Default values](#default-values), [pre-defined formatters](#pre-defined-formatters), and [custom formatters](#registering-formatters) have no effect.
289
+
290
+ class Account
291
+ has_value :logins, :klass => :integer, :default => 0
292
+ end
104
293
 
105
- _aliases_
294
+ >> Account.new(:logins => nil).loginx
295
+ => nil
296
+
297
+ # note this is not the same as
298
+ >> nil.to_i
299
+ => 0
300
+
301
+ Aliases
302
+ -------
303
+
304
+ Set additional getters and setters. Useful when outside data sources have odd field names.
106
305
 
107
306
  # This example requires active_support because of Hash.from_xml
108
307
 
@@ -119,7 +318,8 @@ _aliases_
119
318
 
120
319
  Formatting Input
121
320
  ----------------
122
- _aka light-weight type-casting_
321
+
322
+ The purpose of Valuable's attribute formatting is to ensure that a model's input is "corrected" and ready for use as soon as the class is instantiated. Valuable provides several formatters by default -- :integer, :boolean, and :date are a few of them. You can optionally write your own formatters -- see [Registering Formatters](#registering-formatters)
123
323
 
124
324
  class BaseballPlayer < Valuable
125
325
 
@@ -139,18 +339,33 @@ _aka light-weight type-casting_
139
339
  >> joe.average
140
340
  => 0.25
141
341
 
142
- # Currently supports:
143
- # - integer
144
- # - decimal ( casts to BigDecimal... NOTE: nil remains nil, not 0 as in nil.to_i )
145
- # - string
146
- # - boolean ( NOTE: '0' casts to FALSE... This isn't intuitive, but I would be fascinated to know when this is not the correct behavior. )
147
- # - or any class ( formats as SomeClass.new( ) unless value.is_a?( SomeClass ) )
342
+ Pre-Defined Formatters
343
+ ----------------------
344
+
345
+ see also [Registering Formatters](#registering-formatters)
346
+ - integer ( see [nil values](#nil-values) )
347
+ - decimal ( casts to BigDecimal. see [nil values](#nil-values) )
348
+ - date ( see [nil values](#nil-values) )
349
+ - string
350
+ - boolean ( NOTE: '0' casts to FALSE... I'm not sure whether this is intuitive, but I would be fascinated to know
351
+ when this is not the correct behavior. )
352
+ - or any class ( formats as SomeClass.new( ) unless value.is_a?( SomeClass ) )
353
+
148
354
 
149
355
  Collections
150
356
  -----------
151
357
 
358
+ has_collection :codez
359
+
360
+ is similar to:
361
+
362
+ has_value :codez, :default => []
363
+
364
+ except that it reads better, and that the formatter is applied to the collection's members, not (obviously) the collection. See [Formatting Collections](#formatting-collections) for more details.
365
+
152
366
  class MailingList < Valuable
153
367
  has_collection :emails
368
+ has_collection :messages, :klass => BulkMessage
154
369
  end
155
370
 
156
371
  >> m = MailingList.new
@@ -163,72 +378,102 @@ Collections
163
378
  => m.emails
164
379
  >> [ 'johnathon.e.wright@nasa.gov', 'other.people@wherever.com' ]
165
380
 
166
- _formatting collections_
381
+ Formatting Collections
382
+ ----------------------
167
383
 
168
- class Player < Valuable
169
- has_value :first_name
170
- has_value :last_name
171
- has_value :salary
384
+ If a klass is specified, members of the collection will be formatted appropriately:
385
+
386
+ >> m.messages << "Houston, we have a problem"
387
+
388
+ >> m.messages.first.class
389
+ => BulkMessage
390
+
391
+ see [Advanced Collection Formatting](#advanced-collection-formatting) for more complex examples.
392
+
393
+ Registering Formatters
394
+ ----------------------
395
+
396
+ If the default formatters don't suit your needs, Valuable allows you to write your own formatting code via register_formatter. You can even override the predefined formatters simply by registering a formatter with the same name.
397
+
398
+ # In honor of NASA's Curiosity rover, let's say you were modeling
399
+ # a rover. Here's the valuable class:
400
+
401
+ class Rover < Valuable
402
+ has_value :orientation
172
403
  end
173
-
174
- class Team < Valuable
175
- has_value :name
176
- has_value :long_name
177
404
 
178
- has_collection :players, :klass => Player
405
+ Sometimes orientation comes in as 'N', 'E', 'S' or 'W', sometimes it comes in as an orientation in degrees as a string ("92"), and sometimes it comes in as an integer. Let's create a formatter that makes sure everything is formatted in degrees. Notice that we're registering this formatter on Valuable, not on Rover. It will be available to every Valuable model.
406
+
407
+ Valuable.register_formatter(:orientation) do |value|
408
+ case value
409
+ when Numeric
410
+ value
411
+ when /^\d{1,3}$/
412
+ value.to_i
413
+ when 'N', 'North'
414
+ 0
415
+ when 'E', 'East'
416
+ 90
417
+ when 'S', 'South'
418
+ 180
419
+ when 'W', 'West'
420
+ 270
421
+ else
422
+ nil
423
+ end
179
424
  end
180
425
 
181
- t = Team.new(:name => 'Toronto', :long_name => 'The Toronto Blue Jays',
182
- 'players' => [
183
- {'first_name' => 'Chad', 'last_name' => 'Beck', :salary => 'n/a'},
184
- {'first_name' => 'Shawn', 'last_name' => 'Camp', :salary => '2250000'},
185
- {'first_name' => 'Brett', 'last_name' => 'Cecil', :salary => '443100'},
186
- Player.new(:first_name => 'Travis', :last_name => 'Snider', :salary => '435800')
187
- ])
426
+ and then we update rover to use the new formatter:
188
427
 
189
- >> t.players.first
190
- => #<Player:0x7fa51e4a1da0 @attributes={:salary=>"n/a", :first_name=>"Chad", :last_name=>"Beck"}>
428
+ class Rover < Valuable
429
+ has_value :orientation, :klass => :orientation
430
+ end
191
431
 
192
- >> t.players.last
193
- => #<Player:0x7fa51ea6a9f8 @attributes={:salary=>"435800", :first_name=>"Travis", :last_name=>"Snider"}>
432
+ >> Rover.new(:orientation => 90).orientation
433
+ => 90
194
434
 
195
- parse_with parses each item in a collection...
435
+ >> Rover.new(:orientation => '282').orientation
436
+ >> 282
196
437
 
197
- class Roster < Valuable
198
- has_collection :players, :klass => Player, :parse_with => :find_by_name
199
- end
438
+ >> Rover.new(:orientation => 'S').orientation
439
+ => 180
200
440
 
201
- Advanced Defaults
202
- -----------------
441
+ More about Attributesd
442
+ ---------------------
203
443
 
204
- class Borg < Valuable
205
- cattr_accessor :count
206
- has_value :position, :default => lambda { Borg.count += 1 }
207
-
208
- def designation
209
- "#{self.position} of #{Borg.count}"
210
- end
444
+ Access the attributes via the attributes hash. Only default and specified attributes will have entries here.
445
+
446
+ class Person < Valuable
447
+ has_value :name
448
+ has_value :is_developer, :default => false
449
+ has_value :ssn
211
450
  end
212
451
 
213
- >> Borg.count = 6
214
- >> seven = Borg.new
215
- >> Borg.count = 9
216
- >> seven.designation
217
- => '7 of 9'
452
+ >> elvis = Person.new(:name => 'The King')
218
453
 
219
- Note -- if you overwrite the constructor, you should call initialize_attributes. Otherwise, your default values won't be set up until the first time the attributes hash is called -- in theory, this could be well after initialization, and could cause unknowable gremlins. Trivial example:
454
+ >> elvis.attributes
455
+ => {:name=>"The King", :is_developer=>false}
220
456
 
221
- class Person
222
- has_value :created_at, :default => lambda { Time.now }
457
+ >> elvis.attributes[:name]
458
+ => "The King"
223
459
 
224
- def initialize(atts)
225
- end
226
- end
460
+ >> elvis.ssn
461
+ => nil
227
462
 
228
- >> p = Person.new
229
- >> # wait 10 minutes
230
- >> p.created_at == Time.now # attributes initialized on first use
231
- => true
463
+ >> elvis.attributes.has_key?(:ssn)
464
+ => false
465
+
466
+ >> elvis.ssn = '409-52-2002' # allegedly
467
+
468
+ >> elvis.attributes[:ssn]
469
+ => "409-52-2002"
470
+
471
+ You _can_ write directly to the attributes hash. As far as I know, Valuable will not care. However, formatters will not be applied.
472
+
473
+ Get a list of all the defined attributes from the class:
474
+
475
+ >> Person.attributes
476
+ => [:name, :is_developer, :ssn]
232
477
 
233
478
  Advanced Input Parsing
234
479
  ----------------------
@@ -289,32 +534,76 @@ Parse via lambda:
289
534
  >> best_movie_ever.title
290
535
  => "The Usual Suspects"
291
536
 
292
- More about Attributes
293
- ---------------------
537
+ Advanced Defaults
538
+ -----------------
294
539
 
295
- Access the attributes via the attributes hash. Only default and specified attributes will have entries here.
540
+ The :default option will accept a lambda and call it on instantiation.
296
541
 
297
- class Person < Valuable
298
- has_value :name
299
- has_value :is_developer, :default => false
300
- has_value :ssn
542
+ class Borg < Valuable
543
+ cattr_accessor :count
544
+ has_value :position, :default => lambda { Borg.count += 1 }
545
+
546
+ def designation
547
+ "#{self.position} of #{Borg.count}"
548
+ end
301
549
  end
302
550
 
303
- >> elvis = Person.new(:name => 'The King')
551
+ >> Borg.count = 6
552
+ >> seven = Borg.new
553
+ >> Borg.count = 9
554
+ >> seven.designation
555
+ => '7 of 9'
304
556
 
305
- >> elvis.attributes
306
- => {:name=>"The King", :is_developer=>false}
557
+ **Caution** -- if you overwrite the constructor, you should call initialize_attributes. Otherwise, your default values won't be set up until the first time the attributes hash is called -- in theory, this could be well after initialization, and could cause unknowable gremlins. Trivial example:
307
558
 
308
- >> elvis.attributes[:name]
309
- => "The King"
559
+ class Person
560
+ has_value :created_at, :default => lambda { Time.now }
310
561
 
311
- >> elvis.ssn
312
- => nil
562
+ def initialize(atts)
563
+ end
564
+ end
313
565
 
314
- Get a list of all the defined attributes from the class:
566
+ >> p = Person.new
567
+ >> # wait 10 minutes
568
+ >> p.created_at == Time.now # attributes initialized on first use
569
+ => true
315
570
 
316
- >> Person.attributes
317
- => [:name, :is_developer, :ssn]
571
+ Advanced Collection Formatting
572
+ ------------------------------
573
+
574
+ see [Collections](#collections) and [Formatting Collections](#formatting-collections) for basic examples. A more complex example involves nested Valuable models:
575
+
576
+ class Team < Valuable
577
+ has_value :name
578
+ has_value :long_name
579
+
580
+ has_collection :players, :klass => Player
581
+ end
582
+
583
+ class Player < Valuable
584
+ has_value :first_name
585
+ has_value :last_name
586
+ has_value :salary
587
+ end
588
+
589
+ t = Team.new(:name => 'Toronto', :long_name => 'The Toronto Blue Jays',
590
+ 'players' => [
591
+ {'first_name' => 'Chad', 'last_name' => 'Beck', :salary => 'n/a'},
592
+ {'first_name' => 'Shawn', 'last_name' => 'Camp', :salary => '2250000'},
593
+ {'first_name' => 'Brett', 'last_name' => 'Cecil', :salary => '443100'},
594
+ Player.new(:first_name => 'Travis', :last_name => 'Snider', :salary => '435800')
595
+ ])
596
+
597
+ >> t.players.first
598
+ => #<Player:0x7fa51e4a1da0 @attributes={:salary=>"n/a", :first_name=>"Chad", :last_name=>"Beck"}>
599
+
600
+ >> t.players.last
601
+ => #<Player:0x7fa51ea6a9f8 @attributes={:salary=>"435800", :first_name=>"Travis", :last_name=>"Snider"}>
602
+
603
+ parse_with parses each item in a collection...
604
+
605
+ class Roster < Valuable
606
+ has_collection :players, :klass => Player, :parse_with => :find_by_name
607
+ end
318
608
 
319
- It's a relatively simple tool that lets you create models with a (hopefully) intuitive syntax, prevents you from writing yet another obvious constructor, and allows you to keep your brain focused on your app.
320
609
 
@@ -37,6 +37,9 @@ module Valuable::Utils
37
37
  klass = collection_item ? attributes[name][:item_klass] : attributes[name][:klass]
38
38
 
39
39
  case klass
40
+ when *formatters.keys
41
+ formatters[klass].call(value)
42
+
40
43
  when NilClass
41
44
 
42
45
  if Proc === attributes[name][:parse_with]
@@ -67,7 +70,7 @@ module Valuable::Utils
67
70
 
68
71
  when :integer
69
72
 
70
- value && value.to_i
73
+ value.to_i if value && value != ''
71
74
 
72
75
  when :decimal
73
76
 
@@ -100,12 +103,16 @@ module Valuable::Utils
100
103
  klass.send( attributes[name][:parse_with] || :new, value)
101
104
  end
102
105
 
103
- end
106
+ end unless value.nil?
104
107
 
105
108
  end
106
-
109
+
110
+ def formatters
111
+ @formatters ||= {}
112
+ end
113
+
107
114
  def klass_options
108
- [NilClass, :integer, Class, :date, :decimal, :string, :boolean]
115
+ [NilClass, :integer, Class, :date, :decimal, :string, :boolean] + formatters.keys
109
116
  end
110
117
 
111
118
  def known_options
data/lib/valuable.rb CHANGED
@@ -43,7 +43,8 @@ class Valuable
43
43
  def attributes
44
44
  @attributes ||= Valuable::Utils.initial_copy_of_attributes(self.class.defaults)
45
45
  end
46
- alias_method :initialize_attributes, :attributes
46
+ alias_method :initialize_attributes, :attributes
47
+ # alias is for readability in constructor
47
48
 
48
49
  # accepts an optional hash that will be used to populate the
49
50
  # predefined attributes for this class.
@@ -279,6 +280,40 @@ class Valuable
279
280
  sudo_alias "#{options[:alias]}=", "#{name}=" if options[:alias]
280
281
  end
281
282
 
283
+ # Register custom formatters. Not happy with the default behavior?
284
+ # Custom formatters override all pre-defined formatters. However,
285
+ # remember that formatters are defined globally, rather than
286
+ # per-class.
287
+ #
288
+ # Valuable.register_formatter(:orientation) do |value|
289
+ # case value
290
+ # case Numeric
291
+ # value
292
+ # when 'N', 'North'
293
+ # 0
294
+ # when 'E', 'East'
295
+ # 90
296
+ # when 'S', 'South'
297
+ # 180
298
+ # when 'W', 'West'
299
+ # 270
300
+ # else
301
+ # nil
302
+ # end
303
+ # end
304
+ #
305
+ # class MarsRover < Valuable
306
+ # has_value :orientation, :klass => :orientation
307
+ # end
308
+ #
309
+ # >> curiosity = MarsRover.new(:orientation => 'S')
310
+ # >> curiosity.orientation
311
+ # => 180
312
+ def register_formatter(name, &block)
313
+ Valuable::Utils.formatters[name] = block
314
+ end
315
+
316
+
282
317
  # Instructs the class NOT to complain if any attributes are set
283
318
  # that haven't been declared.
284
319
  #
@@ -0,0 +1,41 @@
1
+ $: << File.expand_path(File.dirname(__FILE__) + '/../lib')
2
+
3
+ require 'test/unit'
4
+ require 'valuable.rb'
5
+
6
+ Valuable.register_formatter(:point) do |latitude, longitude|
7
+ :perfect
8
+ end
9
+
10
+ Valuable.register_formatter(:temperature) do |input|
11
+ if input.nil?
12
+ 'unknown'
13
+ else
14
+ 'very hot'
15
+ end
16
+ end
17
+
18
+ class MarsLander < Valuable
19
+ has_value :position, :klass => :point
20
+ has_value :core_temperature, :klass => :temperature
21
+ end
22
+
23
+ class CustomFormatterTest < Test::Unit::TestCase
24
+
25
+ def test_that_formatter_keys_are_added_to_the_klass_options_list
26
+ assert Valuable::Utils.klass_options.include?( :point )
27
+ end
28
+
29
+ def test_that_custom_formatters_are_used_to_set_attributes
30
+ expected = :perfect
31
+ actual = MarsLander.new(:position => [10, 20]).position
32
+ assert_equal expected, actual
33
+ end
34
+
35
+ def test_that_nil_values_are_not_passed_to_custom_formatter
36
+ expected = nil
37
+ actual = MarsLander.new(:core_temperature => nil).core_temperature
38
+ assert_equal expected, actual
39
+ end
40
+ end
41
+
@@ -66,6 +66,10 @@ class BaseTest < Test::Unit::TestCase
66
66
  assert_equal nil, Developer.new.experience
67
67
  end
68
68
 
69
+ def test_that_integer_attributes_ignore_blanks
70
+ assert_equal nil, Developer.new(:experience => '').experience
71
+ end
72
+
69
73
  def test_that_attributes_can_be_klassified
70
74
  dev = Developer.new(:cubical => 12)
71
75
  assert_equal Cubical, dev.cubical.class
@@ -161,7 +165,7 @@ class BaseTest < Test::Unit::TestCase
161
165
  end
162
166
 
163
167
  def test_that_values_are_cast_to_boolean
164
- assert_equal false, Developer.new(:employed => nil).employed
168
+ assert_equal true, Developer.new(:employed => 'true').employed
165
169
  end
166
170
 
167
171
  def test_that_string_zero_becomes_false
data/valuable.gemspec CHANGED
@@ -4,7 +4,7 @@ version = File.read(File.expand_path("../valuable.version",__FILE__)).strip
4
4
  spec = Gem::Specification.new do |s|
5
5
  s.name = 'valuable'
6
6
  s.version = version
7
- s.summary = "attr_accessor on steroids with defaults, default constructor, and attribute formatting."
7
+ s.summary = "attr_accessor on steroids with defaults, attribute formatting, alias methods, etc."
8
8
  s.description = "Valuable is a ruby base class that is essentially attr_accessor on steroids. A simple and intuitive interface allows you to get on with modeling in your app."
9
9
  s.license = 'MIT'
10
10
 
data/valuable.version CHANGED
@@ -1 +1 @@
1
- 0.9.5
1
+ 0.9.6
metadata CHANGED
@@ -1,33 +1,23 @@
1
- --- !ruby/object:Gem::Specification
1
+ --- !ruby/object:Gem::Specification
2
2
  name: valuable
3
- version: !ruby/object:Gem::Version
4
- hash: 49
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.9.6
5
5
  prerelease:
6
- segments:
7
- - 0
8
- - 9
9
- - 5
10
- version: 0.9.5
11
6
  platform: ruby
12
- authors:
7
+ authors:
13
8
  - Johnathon Wright
14
9
  autorequire:
15
10
  bindir: bin
16
11
  cert_chain: []
17
-
18
- date: 2012-05-08 00:00:00 -05:00
19
- default_executable:
12
+ date: 2012-08-10 00:00:00.000000000 Z
20
13
  dependencies: []
21
-
22
- description: Valuable is a ruby base class that is essentially attr_accessor on steroids. A simple and intuitive interface allows you to get on with modeling in your app.
14
+ description: Valuable is a ruby base class that is essentially attr_accessor on steroids.
15
+ A simple and intuitive interface allows you to get on with modeling in your app.
23
16
  email: jw@mustmodify.com
24
17
  executables: []
25
-
26
18
  extensions: []
27
-
28
19
  extra_rdoc_files: []
29
-
30
- files:
20
+ files:
31
21
  - .gitignore
32
22
  - Gemfile
33
23
  - README.markdown
@@ -40,6 +30,7 @@ files:
40
30
  - lib/valuable/utils.rb
41
31
  - test/alias_test.rb
42
32
  - test/bad_attributes_test.rb
33
+ - test/custom_formatter_test.rb
43
34
  - test/custom_initializer_test.rb
44
35
  - test/default_values_from_anon_methods.rb
45
36
  - test/deprecated_test.rb
@@ -51,39 +42,30 @@ files:
51
42
  - todo.txt
52
43
  - valuable.gemspec
53
44
  - valuable.version
54
- has_rdoc: true
55
45
  homepage: http://valuable.mustmodify.com/
56
- licenses:
46
+ licenses:
57
47
  - MIT
58
48
  post_install_message:
59
49
  rdoc_options: []
60
-
61
- require_paths:
50
+ require_paths:
62
51
  - lib
63
- required_ruby_version: !ruby/object:Gem::Requirement
52
+ required_ruby_version: !ruby/object:Gem::Requirement
64
53
  none: false
65
- requirements:
66
- - - ">="
67
- - !ruby/object:Gem::Version
68
- hash: 3
69
- segments:
70
- - 0
71
- version: "0"
72
- required_rubygems_version: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - ! '>='
56
+ - !ruby/object:Gem::Version
57
+ version: '0'
58
+ required_rubygems_version: !ruby/object:Gem::Requirement
73
59
  none: false
74
- requirements:
75
- - - ">="
76
- - !ruby/object:Gem::Version
77
- hash: 3
78
- segments:
79
- - 0
80
- version: "0"
60
+ requirements:
61
+ - - ! '>='
62
+ - !ruby/object:Gem::Version
63
+ version: '0'
81
64
  requirements: []
82
-
83
65
  rubyforge_project:
84
- rubygems_version: 1.6.2
66
+ rubygems_version: 1.8.11
85
67
  signing_key:
86
68
  specification_version: 3
87
- summary: attr_accessor on steroids with defaults, default constructor, and attribute formatting.
69
+ summary: attr_accessor on steroids with defaults, attribute formatting, alias methods,
70
+ etc.
88
71
  test_files: []
89
-