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 +404 -115
- data/lib/valuable/utils.rb +11 -4
- data/lib/valuable.rb +36 -1
- data/test/custom_formatter_test.rb +41 -0
- data/test/valuable_test.rb +5 -1
- data/valuable.gemspec +1 -1
- data/valuable.version +1 -1
- metadata +24 -42
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
|
-
|
147
|
+
>> party = Party.new(:theme => 'Black and Whitle')
|
14
148
|
|
15
|
-
|
149
|
+
>> party.attributes
|
150
|
+
=> {:theme => 'Black and White', :time => '6pm'}
|
16
151
|
|
17
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
36
|
-
|
37
|
-
'
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
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
|
-
|
61
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
103
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
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
|
-
|
381
|
+
Formatting Collections
|
382
|
+
----------------------
|
167
383
|
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
190
|
-
|
428
|
+
class Rover < Valuable
|
429
|
+
has_value :orientation, :klass => :orientation
|
430
|
+
end
|
191
431
|
|
192
|
-
>>
|
193
|
-
=>
|
432
|
+
>> Rover.new(:orientation => 90).orientation
|
433
|
+
=> 90
|
194
434
|
|
195
|
-
|
435
|
+
>> Rover.new(:orientation => '282').orientation
|
436
|
+
>> 282
|
196
437
|
|
197
|
-
|
198
|
-
|
199
|
-
end
|
438
|
+
>> Rover.new(:orientation => 'S').orientation
|
439
|
+
=> 180
|
200
440
|
|
201
|
-
|
202
|
-
|
441
|
+
More about Attributesd
|
442
|
+
---------------------
|
203
443
|
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
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
|
-
>>
|
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
|
-
|
454
|
+
>> elvis.attributes
|
455
|
+
=> {:name=>"The King", :is_developer=>false}
|
220
456
|
|
221
|
-
|
222
|
-
|
457
|
+
>> elvis.attributes[:name]
|
458
|
+
=> "The King"
|
223
459
|
|
224
|
-
|
225
|
-
|
226
|
-
end
|
460
|
+
>> elvis.ssn
|
461
|
+
=> nil
|
227
462
|
|
228
|
-
>>
|
229
|
-
|
230
|
-
|
231
|
-
|
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
|
-
|
293
|
-
|
537
|
+
Advanced Defaults
|
538
|
+
-----------------
|
294
539
|
|
295
|
-
|
540
|
+
The :default option will accept a lambda and call it on instantiation.
|
296
541
|
|
297
|
-
class
|
298
|
-
|
299
|
-
has_value :
|
300
|
-
|
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
|
-
>>
|
551
|
+
>> Borg.count = 6
|
552
|
+
>> seven = Borg.new
|
553
|
+
>> Borg.count = 9
|
554
|
+
>> seven.designation
|
555
|
+
=> '7 of 9'
|
304
556
|
|
305
|
-
|
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
|
-
|
309
|
-
|
559
|
+
class Person
|
560
|
+
has_value :created_at, :default => lambda { Time.now }
|
310
561
|
|
311
|
-
|
312
|
-
|
562
|
+
def initialize(atts)
|
563
|
+
end
|
564
|
+
end
|
313
565
|
|
314
|
-
|
566
|
+
>> p = Person.new
|
567
|
+
>> # wait 10 minutes
|
568
|
+
>> p.created_at == Time.now # attributes initialized on first use
|
569
|
+
=> true
|
315
570
|
|
316
|
-
|
317
|
-
|
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
|
|
data/lib/valuable/utils.rb
CHANGED
@@ -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
|
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
|
+
|
data/test/valuable_test.rb
CHANGED
@@ -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
|
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,
|
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.
|
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
|
-
|
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
|
-
|
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
|
-
|
69
|
-
|
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
|
-
|
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.
|
66
|
+
rubygems_version: 1.8.11
|
85
67
|
signing_key:
|
86
68
|
specification_version: 3
|
87
|
-
summary: attr_accessor on steroids with defaults,
|
69
|
+
summary: attr_accessor on steroids with defaults, attribute formatting, alias methods,
|
70
|
+
etc.
|
88
71
|
test_files: []
|
89
|
-
|