valuable 0.9.2 → 0.9.3

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -7,3 +7,4 @@
7
7
  pkg
8
8
  vendor
9
9
  Gemfile.lock
10
+ *.gem
data/Gemfile CHANGED
@@ -3,6 +3,8 @@ source 'http://rubygems.org'
3
3
  # Specify your gem's dependencies in sketch.gemspec
4
4
  gemspec
5
5
 
6
+ gem 'rake'
7
+
6
8
  group :test do
7
9
  gem 'test-unit'
8
10
  gem 'mocha'
data/README.markdown CHANGED
@@ -1,9 +1,14 @@
1
1
  Introducing Valuable
2
2
  ====================
3
3
 
4
- Valuable enables quick modeling... it's attr_accessor on steroids. For all those times you wanted to use OO to model something but it seemed like too much of a pain, try Valuable. Its simple interface allows you to model without hassles, so you can get on with the logic specific to your application.
4
+ Valuable enables quick modeling... it's attr_accessor on steroids. Its simple interface allows you to build, change and discard models without hassles, so you can get on with the logic specific to your application.
5
5
 
6
- Frequent Uses:
6
+ Valuable provides DRY decoration like attr_accessor, but includes default values and other formatting (like, "2" => 2), and a constructor that accepts an attributes hash. It provides a class-level list of attributes, an instance-level attributes hash, and more.
7
+
8
+ Tested with "Rubinius":http://www.rubini.us, 1.8.7, 1.9.1, 1.9.2, 1.9.3
9
+
10
+ Frequent Uses
11
+ -------------
7
12
 
8
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."
9
14
 
@@ -11,10 +16,8 @@ Frequent Uses:
11
16
 
12
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.
13
18
 
14
- Valuable provides DRY decoration like attr_accessor, but includes default values and other formatting (like, "2" => 2), and a constructor that accepts an attributes hash. It provides a class-level list of attributes, an instance-level attributes hash, and more.
15
-
16
19
  Type Casting in Ruby? You must be crazy...
17
- -------------------------------------------------------------
20
+ ------------------------------------------
18
21
  Yeah, I get that alot. I mean, about type casting. I'm not writing
19
22
  C# over here. Rails does it, they just don't call it type casting,
20
23
  so no one complains when they pass in "2" as a parameter and mysteriously
@@ -48,23 +51,11 @@ you'll end up with this:
48
51
  >> p.phone_number.class
49
52
  => PhoneNumber
50
53
 
51
- "Yeah, I could have just done that myself."
52
- "Right, but now you don't have to."
53
-
54
- Default Values
55
- --------------
56
- 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.
57
-
58
- When a default value and a klass are specified, the default value will NOT be cast to type klass -- you must do it.
59
-
60
- If a value having a default is set to null after it is constructed, it will NOT be set to the default.
61
-
62
- 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.
63
-
64
- Examples
65
- -------
54
+ "Yeah, I could have just done that myself."
55
+ "Right, but now you don't have to."
66
56
 
67
- _basic syntax_
57
+ Basic Syntax
58
+ ------------
68
59
 
69
60
  class Fruit < Valuable
70
61
  has_value :name
@@ -83,6 +74,16 @@ _constructor accepts an attributes hash_
83
74
 
84
75
  _default values_
85
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.
80
+
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.
86
+
86
87
  class Developer
87
88
  has_value :name
88
89
  has_value :nickname, :default => 'mort'
@@ -101,7 +102,25 @@ _setting a value to nil overrides the default._
101
102
  >> Developer.new(:name => 'KDD', :nickname => nil).nickname
102
103
  => nil
103
104
 
104
- _formatting aka light-weight type-casting_
105
+ _aliases_
106
+
107
+ # This example requires active_support because of Hash.from_xml
108
+
109
+ class Software < Valuable
110
+ has_value :name, :alias => 'Title'
111
+ end
112
+
113
+ >> xml = '<software><Title>Windows XP</Title></software>'
114
+
115
+ >> xp = Software.new(Hash.from_xml(xml)['software'])
116
+
117
+ >> xp.name
118
+ => "Windows XP"
119
+
120
+
121
+ Formatting Input
122
+ ----------------
123
+ _aka light-weight type-casting_
105
124
 
106
125
  class BaseballPlayer < Valuable
107
126
 
@@ -125,11 +144,11 @@ _formatting aka light-weight type-casting_
125
144
  # - integer
126
145
  # - decimal ( casts to BigDecimal... NOTE: nil remains nil, not 0 as in nil.to_i )
127
146
  # - string
128
- # - boolean ( NOTE: '0' casts to FALSE... I would be fascinated to know when this is not the correct behavior. )
147
+ # - boolean ( NOTE: '0' casts to FALSE... This isn't intuitive, but I would be fascinated to know when this is not the correct behavior. )
129
148
  # - or any class ( formats as SomeClass.new( ) unless value.is_a?( SomeClass ) )
130
149
 
131
-
132
- _collections_
150
+ Collections
151
+ -----------
133
152
 
134
153
  class MailingList < Valuable
135
154
  has_collection :emails
@@ -174,78 +193,93 @@ _formatting collections_
174
193
  >> t.players.last
175
194
  => #<Player:0x7fa51ea6a9f8 @attributes={:salary=>"435800", :first_name=>"Travis", :last_name=>"Snider"}>
176
195
 
177
- _aliases_
178
-
179
- # This example requires active_support because of Hash.from_xml
196
+ parse_with parses each item in a collection...
180
197
 
181
- class Software < Valuable
182
- has_value :name, :alias => 'Title'
198
+ class Roster < Valuable
199
+ has_collection :players, :klass => Player, :parse_with => :find_by_name
183
200
  end
184
201
 
185
- >> xml = '<software><Title>Windows XP</Title></software>'
202
+ Advanced Defaults
203
+ -----------------
186
204
 
187
- >> xp = Software.new(Hash.from_xml(xml)['software'])
205
+ class Borg < Valuable
206
+ cattr_accessor :count
207
+ has_value :position, :default => lambda { Borg.count += 1 }
208
+
209
+ def designation
210
+ "#{self.position} of #{Borg.count}"
211
+ end
212
+ end
188
213
 
189
- >> xp.name
190
- => "Windows XP"
214
+ >> Borg.count = 6
215
+ >> seven = Borg.new
216
+ >> Borg.count = 9
217
+ >> seven.designation
218
+ => '7 of 9'
191
219
 
220
+ Advanced Input Parsing
221
+ ----------------------
192
222
 
193
- _as a presenter in Rails_
223
+ Sometimes, .to_s isn't enough... the architypical example being Date.parse(value). In these cases, you can specify what class-level method should be used to process the input.
194
224
 
195
- class CalenderPresenter < Valuable
196
- has_value :month, :klass => Integer, :default => Time.now.month
197
- has_value :year, :klass => Integer, :default => Time.now.year
225
+ require 'date'
198
226
 
199
- def start_date
200
- Date.civil( year, month, 1)
201
- end
227
+ class Person < Valuable
228
+ has_value :date_of_birth, :alias => :dob, :klass => Date, :parse_with => :parse
202
229
 
203
- def end_date
204
- Date.civil( year, month, -1) #strange I know
230
+ def age_in_days
231
+ Date.today - dob
205
232
  end
233
+ end
206
234
 
207
- def events
208
- Event.find(:all, :conditions => event_conditions)
209
- end
235
+ >> sammy = Person.new(:dob => '2012-02-17')
236
+ >> sammy.age_in_days
237
+ => Rational(8, 1)
210
238
 
211
- def event_conditions
212
- ['starts_at between ? and ?', start_date, end_date]
213
- end
214
- end
239
+ use it to load associated data from an exising set...
215
240
 
216
- this class might appear in a controller like this:
241
+ class Planet < Valuable
242
+ has_value :name
243
+ has_value :spaceport
217
244
 
218
- class CalendarController < ApplicationController
219
- def show
220
- @presenter = CalendarPresenter.new(params[:calendar])
245
+ def Planet.list
246
+ @list ||= []
221
247
  end
222
- end
223
248
 
224
- but it's easier to understand like this:
249
+ def Planet.find_by_name( needle )
250
+ list.find{|i| i.name == needle }
251
+ end
252
+ end
225
253
 
226
- >> @presenter = CalendarPresenter.new({}) # first pageload
254
+ class Spaceship < Valuable
255
+ has_value :name
256
+ has_value :home, :klass => Planet, :parse_with => :find_by_name
257
+ end
227
258
 
228
- >> @presenter.start_date
229
- => Tue, 01 Dec 2009
259
+ Planet.list << Planet.new(:name => 'Earth', :spaceport => 'KSC')
260
+ Planet.list << Planet.new(:name => 'Mars', :spaceport => 'Olympus Mons')
230
261
 
231
- >> @presenter.end_date
232
- => Thu, 31 Dec 2009
262
+ >> vger = Spaceship.new( :name => "V'ger", :home => 'Earth')
263
+ >> vger.home.spaceport
264
+ => 'KSC'
233
265
 
234
- >> # User selects some other month and year; the next request looks like...
266
+ Parse via lambda:
235
267
 
236
- >> @presenter = CalendarPresenter.new({:month => '2', :year => '2002'})
268
+ require 'active_support'
237
269
 
238
- >> @presenter.start_date
239
- => Fri, 01 Feb 2002
270
+ class Movie < Valuable
271
+ has_value :title, :parse_with => lambda{|x| x.titleize}
272
+ end
240
273
 
241
- >> @presenter.end_date
242
- => Thu, 28 Feb 2002
274
+ >> best_movie_ever = Movie.new(:title => 'the usual suspects')
243
275
 
244
- ...
276
+ >> best_movie_ever.title
277
+ => "The Usual Suspects"
245
278
 
246
- So, if you're reading this, you're probably thinking, "I could have done that!" Yes, it's true. I'll happily agree that it's a relatively simple tool if you'll agree that it lets you model a calendar with an intuitive syntax, prevents you from writing yet another obvious constructor, and allows you to keep your brain focused on your app.
279
+ More about Attributes
280
+ ---------------------
247
281
 
248
- _you can access the attributes via the attributes hash. Only default and specified attributes will have entries here._
282
+ Access the attributes via the attributes hash. Only default and specified attributes will have entries here.
249
283
 
250
284
  class Person < Valuable
251
285
  has_value :name
@@ -264,9 +298,10 @@ _you can access the attributes via the attributes hash. Only default and specifi
264
298
  >> elvis.ssn
265
299
  => nil
266
300
 
267
- _also, you can get a list of all the defined attributes from the class_
301
+ Get a list of all the defined attributes from the class:
268
302
 
269
303
  >> Person.attributes
270
304
  => [:name, :is_developer, :ssn]
271
305
 
306
+ 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.
272
307
 
@@ -0,0 +1,8 @@
1
+ require File.dirname(__FILE__) + '/phone_number'
2
+
3
+ class Person < Valuable
4
+ has_value :name
5
+ has_value :age, :klass => :integer
6
+ has_value :phone_number, :klass => PhoneNumber
7
+ end
8
+
@@ -0,0 +1,51 @@
1
+ # This class might appear in a controller like this:
2
+ #
3
+ # class CalendarController < ApplicationController
4
+ # def show
5
+ # @presenter = CalendarPresenter.new(params[:calendar])
6
+ # end
7
+ # end
8
+ #
9
+ # but in documentation, makes more sense this way :)
10
+ #
11
+ # >> @presenter = CalendarPresenter.new # first pageload
12
+ #
13
+ # >> @presenter.start_date
14
+ # => Tue, 01 Dec 2009
15
+ #
16
+ # >> @presenter.end_date
17
+ # => Thu, 31 Dec 2009
18
+ #
19
+ # >> # User selects some other month and year; the next request looks like...
20
+ #
21
+ # >> @presenter = CalendarPresenter.new({:month => '2', :year => '2002'})
22
+ #
23
+ # >> @presenter.start_date
24
+ # => Fri, 01 Feb 2002
25
+ #
26
+ # >> @presenter.end_date
27
+ # => Thu, 28 Feb 2002
28
+ #
29
+ # ...
30
+ #
31
+ class CalenderPresenter < Valuable
32
+ has_value :month, :klass => Integer, :default => Time.now.month
33
+ has_value :year, :klass => Integer, :default => Time.now.year
34
+
35
+ def start_date
36
+ Date.civil( year, month, 1)
37
+ end
38
+
39
+ def end_date
40
+ Date.civil( year, month, -1) #strange I know
41
+ end
42
+
43
+ def events
44
+ Event.find(:all, :conditions => event_conditions)
45
+ end
46
+
47
+ def event_conditions
48
+ ['starts_at between ? and ?', start_date, end_date]
49
+ end
50
+ end
51
+
data/lib/valuable.rb CHANGED
@@ -19,29 +19,39 @@
19
19
  #
20
20
  class Valuable
21
21
 
22
- # Returns a Hash representing all known values. Values are set three ways:
22
+ # Returns a Hash representing all known values. Values are set four ways:
23
23
  #
24
- # (1) a default value
24
+ # (1) Default values are set on instanciation, ie Person.new
25
25
  # (2) they were passed to the constructor
26
26
  # Bus.new(:color => 'green')
27
27
  # (3) they were set via their namesake setter or alias setter
28
28
  # bus.color = 'green'
29
29
  # bus.Passengers = ['bill', 'steve']
30
- #
30
+ # (4) the write_attributes(key, value) method
31
+ #
31
32
  # Values that have not been set and have no default not appear in this
32
33
  # collection. Their namesake attribute methods will respond with nil.
33
- # Always use symbols to access these values.
34
- #
34
+ # Always use symbols to access these values, ie:
35
+ # Person.attributes[:color]
36
+ # not
37
+ # Person.attributes['color']
38
+ #
39
+ # basic usage:
35
40
  # >> bus = Bus.new(:number => 16) # color has default value 'yellow'
36
41
  # >> bus.attributes
37
42
  # => {:color => 'yellow', :number => 16}
38
43
  def attributes
39
- @attributes ||= Valuable::Utils.deep_duplicate_of(self.class.defaults)
44
+ @attributes
45
+ end
46
+
47
+ def initialize_attributes
48
+ @attributes ||= Valuable::Utils.initial_copy_of_attributes(self.class.defaults)
40
49
  end
41
50
 
42
51
  # accepts an optional hash that will be used to populate the
43
52
  # predefined attributes for this class.
44
53
  def initialize(atts = nil)
54
+ initialize_attributes
45
55
  self.update_attributes(atts || {})
46
56
  end
47
57
 
@@ -15,6 +15,20 @@ module Valuable::Utils
15
15
  end
16
16
  end
17
17
 
18
+ def initial_copy_of_attributes(atts)
19
+ out = {}
20
+ atts.each do |name, value|
21
+ case value
22
+ when Proc
23
+ out[name] = value.call
24
+ else
25
+ out[name] = deep_duplicate_of( value )
26
+ end
27
+ end
28
+
29
+ out
30
+ end
31
+
18
32
  def deep_duplicate_of(value)
19
33
  Marshal.load(Marshal.dump(value))
20
34
  end
@@ -25,7 +39,11 @@ module Valuable::Utils
25
39
  case klass
26
40
  when NilClass
27
41
 
28
- value
42
+ if Proc === attributes[name][:parse_with]
43
+ attributes[name][:parse_with].call(value)
44
+ else
45
+ value
46
+ end
29
47
 
30
48
  when :collection
31
49
  if( value.kind_of?(Array) )
@@ -77,7 +95,7 @@ module Valuable::Utils
77
95
  elsif value.is_a? klass
78
96
  value
79
97
  else
80
- klass.new(value)
98
+ klass.send( attributes[name][:parse_with] || :new, value)
81
99
  end
82
100
 
83
101
  end
@@ -89,7 +107,7 @@ module Valuable::Utils
89
107
  end
90
108
 
91
109
  def known_options
92
- [:klass, :default, :negative, :alias]
110
+ [:klass, :default, :negative, :alias, :parse_with]
93
111
  end
94
112
 
95
113
  # this helper raises an exception if the options passed to has_value
@@ -101,6 +119,8 @@ module Valuable::Utils
101
119
  raise ArgumentError, "has_value did not know how to respond to option(s) #{invalid_options.join(', ')}. Valid (optional) arguments are: #{known_options.join(', ')}" unless invalid_options.empty?
102
120
 
103
121
  raise ArgumentError, "#{class_name} doesn't know how to format #{attribute} with :klass => #{options[:klass].inspect}" unless klass_options.any?{|klass| klass === options[:klass]}
122
+
123
+ raise( ArgumentError, "#{class_name} can't promise to return a(n) #{options[:klass]} when using :parse_with" ) if options[:klass].is_a?( Symbol ) && options[:parse_with]
104
124
  end
105
125
  end
106
126
  end
@@ -0,0 +1,29 @@
1
+ $: << File.expand_path(File.dirname(__FILE__) + '/../lib')
2
+
3
+ require 'rubygems'
4
+ require 'test/unit'
5
+ require 'valuable.rb'
6
+
7
+ class Borg < Valuable
8
+ class << self
9
+ attr_accessor :count
10
+ end
11
+ has_value :position, :default => lambda { Borg.count += 1 }
12
+ has_value :name
13
+
14
+ def designation
15
+ "#{self.position} of #{Borg.count}"
16
+ end
17
+ end
18
+
19
+ class DefaultValueFromAnonMethodsTest < Test::Unit::TestCase
20
+
21
+ def test_that_children_inherit_their_parents_attributes
22
+ Borg.count = 6
23
+ seven = Borg.new
24
+ Borg.count = 9
25
+ assert_equal '7 of 9', seven.designation
26
+ end
27
+
28
+ end
29
+
@@ -0,0 +1,47 @@
1
+ $: << File.expand_path(File.dirname(__FILE__) + '/../lib')
2
+
3
+ require 'test/unit'
4
+ require 'valuable.rb'
5
+ require 'mocha'
6
+
7
+ class Person < Valuable
8
+ has_value :first_name
9
+ has_value :last_name
10
+
11
+ def Person.load( name )
12
+ f, l = name.split(' ') # trivial case
13
+ new(:first_name => f, :last_name => l)
14
+ end
15
+ end
16
+
17
+ class RailsApp < Valuable
18
+ has_value :tech_lead, :klass => Person, :parse_with => :load
19
+ has_collection :devs, :klass => Person, :parse_with => :load
20
+ has_value :name, :parse_with => lambda{|x| x == 'IA' ? 'Information Architecture' : x}
21
+ end
22
+
23
+ class ParseWithTest < Test::Unit::TestCase
24
+
25
+ def test_that_parse_with_calls_target_classes_parse_method
26
+ ia = RailsApp.new(:tech_lead => 'Adam Dalton')
27
+ assert_equal 'Adam', ia.tech_lead.first_name
28
+ end
29
+
30
+ def test_that_collections_are_parsed
31
+ ia = RailsApp.new(:devs => ['Dennis Camp', 'Richard Hoblitzell', 'Paul Kuracz', 'Magda Lueiro', 'George Meyer', 'David Moyer', 'Bill Snoddy'])
32
+ expected = ['Dennis', 'Richard', 'Paul', 'Magda', 'George', 'David', 'Bill']
33
+ actual = ia.devs.map(&:first_name)
34
+ assert_equal expected, actual
35
+ end
36
+
37
+ def test_that_lambdas_can_be_used_as_parsers
38
+ assert_equal 'Information Architecture', RailsApp.new(:name => 'IA').name
39
+ end
40
+
41
+ def test_that_it_raises_an_error_when_passed_a_class_and_a_proc
42
+ animal = Class.new(Valuable)
43
+ assert_raises ArgumentError, "Class can't promise to return a(n) :integer when using the option :parse_with" do
44
+ animal.has_value :invalid, :klass => :integer, :parse_with => :method
45
+ end
46
+ end
47
+ end
data/todo.txt CHANGED
@@ -12,17 +12,6 @@ class SomeSearchThing < Valuable
12
12
 
13
13
  end
14
14
 
15
- class Datum < Valuable
16
- has_index :by_name
17
- has_list :calculations
18
- end
19
- -or-
20
- class Datum < Valuable
21
- lists :calculations
22
- indexes :by_name
23
- end
24
-
25
-
26
15
  BEST PRACTICES DOC
27
16
 
28
17
  When creating a searcher:
@@ -78,6 +67,18 @@ like this:
78
67
  end
79
68
 
80
69
 
81
- what about has_value :number, :construct => 'Decimal#parse'
70
+
71
+ :klass => :decimal
72
+ - When is decimal available? Only load then.
73
+ - If calling :decimal and it hasn't loaded, error
74
+
75
+
76
+
77
+ class Change < Decimal
78
+ extend Valuable::whatever or
79
+ extend Valuable
80
+
81
+ ...
82
+ end
82
83
 
83
84
  - do a better job with method_missing
data/valuable.version CHANGED
@@ -1 +1 @@
1
- 0.9.2
1
+ 0.9.3
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: valuable
3
3
  version: !ruby/object:Gem::Version
4
- hash: 63
4
+ hash: 61
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
8
  - 9
9
- - 2
10
- version: 0.9.2
9
+ - 3
10
+ version: 0.9.3
11
11
  platform: ruby
12
12
  authors:
13
13
  - Johnathon Wright
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2011-09-27 00:00:00 -05:00
18
+ date: 2012-02-25 00:00:00 -06:00
19
19
  default_executable:
20
20
  dependencies: []
21
21
 
@@ -33,13 +33,17 @@ files:
33
33
  - README.markdown
34
34
  - Rakefile
35
35
  - examples/baseball.rb
36
+ - examples/person.rb
36
37
  - examples/phone_number.rb
38
+ - examples/rails_presenter.rb
37
39
  - lib/valuable.rb
38
40
  - lib/valuable/utils.rb
39
41
  - test/alias_test.rb
40
42
  - test/bad_attributes_test.rb
43
+ - test/default_values_from_anon_methods.rb
41
44
  - test/deprecated_test.rb
42
45
  - test/inheritance_test.rb
46
+ - test/parse_with_test.rb
43
47
  - test/typical_test.rb
44
48
  - test/valuable_test.rb
45
49
  - test/write_and_read_attribute_test.rb
@@ -76,7 +80,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
76
80
  requirements: []
77
81
 
78
82
  rubyforge_project:
79
- rubygems_version: 1.4.2
83
+ rubygems_version: 1.6.2
80
84
  signing_key:
81
85
  specification_version: 3
82
86
  summary: attr_accessor on steroids with defaults, default constructor, and attribute formatting.