Valuable 0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README.markdown +169 -0
- data/lib/valuable.rb +233 -0
- data/rakefile.rb +76 -0
- data/test/bad_attributes_test.rb +23 -0
- data/test/deprecated_test.rb +80 -0
- data/test/inheritance_test.rb +28 -0
- data/test/valuable_test.rb +194 -0
- metadata +60 -0
data/README.markdown
ADDED
@@ -0,0 +1,169 @@
|
|
1
|
+
Introducing Valuable
|
2
|
+
====================
|
3
|
+
|
4
|
+
Valuable enables quick modeling... it's attr_accessor on steroids. It intends to use a simple and intuitive interface, allowing you easily create models without hassles, so you can get on with the logic specific to your application. I find myself using it in sort of a presenter capacity, when I have to pull data from non-standard data sources, and to handle temporary data during imports.
|
5
|
+
|
6
|
+
Valuable provides DRY decoration like attr_accessor, but includes default values, light weight type casting 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
|
+
Examples
|
9
|
+
-------
|
10
|
+
|
11
|
+
_basic syntax_
|
12
|
+
|
13
|
+
class Fruit
|
14
|
+
has_value :name
|
15
|
+
has_collection :vitamins
|
16
|
+
end
|
17
|
+
|
18
|
+
_constructor accepts an attributes hash_
|
19
|
+
|
20
|
+
>> apple = Fruit.new(:name => 'Apple')
|
21
|
+
|
22
|
+
>> apple.name
|
23
|
+
=> 'Apple'
|
24
|
+
|
25
|
+
>> apple.vitamins
|
26
|
+
=> []
|
27
|
+
|
28
|
+
_default values_
|
29
|
+
|
30
|
+
class Developer
|
31
|
+
has_value :name
|
32
|
+
has_value :nickname, :default => 'mort'
|
33
|
+
end
|
34
|
+
|
35
|
+
>> dev = Developer.new(:name => 'zk')
|
36
|
+
|
37
|
+
>> dev.name
|
38
|
+
=> 'zk'
|
39
|
+
|
40
|
+
>> dev.nickname
|
41
|
+
=> 'mort'
|
42
|
+
|
43
|
+
_setting a value to nil overrides the default._
|
44
|
+
|
45
|
+
>> Developer.new(:name => 'KDD', :nickname => nil).nickname
|
46
|
+
=> nil
|
47
|
+
|
48
|
+
_light weight type casting_
|
49
|
+
|
50
|
+
class BaseballPlayer < Valuable
|
51
|
+
|
52
|
+
has_value :at_bats, :klass => :integer
|
53
|
+
has_value :hits, :klass => :integer
|
54
|
+
|
55
|
+
def average
|
56
|
+
hits/at_bats.to_f if hits && at_bats
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
>> joe = BaseballPlayer.new(:hits => '5', :at_bats => '20', :on_drugs => '0' == '1')
|
61
|
+
|
62
|
+
>> joe.at_bats
|
63
|
+
=> 20
|
64
|
+
|
65
|
+
>> joe.average
|
66
|
+
=> 0.25
|
67
|
+
|
68
|
+
_I find myself using classes to format things... ( PhoneNumber is provided in `/examples` )_
|
69
|
+
|
70
|
+
class School < Valuable
|
71
|
+
has_value :name
|
72
|
+
has_value :phone, :klass => PhoneNumber
|
73
|
+
end
|
74
|
+
|
75
|
+
>> School.new(:name => 'Vanderbilt', :phone => '3332223333').phone
|
76
|
+
=> '(333) 222-3333'
|
77
|
+
|
78
|
+
_as a presenter in Rails_
|
79
|
+
|
80
|
+
class CalenderPresenter < Valuable
|
81
|
+
has_value :month, :klass => Integer, :default => Time.now.month
|
82
|
+
has_value :year, :klass => Integer, :default => Time.now.year
|
83
|
+
|
84
|
+
def start_date
|
85
|
+
Date.civil( year, month, 1)
|
86
|
+
end
|
87
|
+
|
88
|
+
def end_date
|
89
|
+
Date.civil( year, month, -1) #strange I know
|
90
|
+
end
|
91
|
+
|
92
|
+
def events
|
93
|
+
Event.find(:all, :conditions => event_conditions)
|
94
|
+
end
|
95
|
+
|
96
|
+
def event_conditions
|
97
|
+
['starts_at between ? and ?', start_date, end_date]
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
_this class might appear in a controller like this:_
|
102
|
+
|
103
|
+
class CalendarController < ApplicationController
|
104
|
+
def show
|
105
|
+
@presenter = CalendarPresenter.new(params[:calendar])
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
_but it's easier to understand like this:_
|
110
|
+
|
111
|
+
>> @presenter = CalendarPresenter.new({}) # first pageload
|
112
|
+
|
113
|
+
>> @presenter.start_date
|
114
|
+
=> Tue, 01 Dec 2009
|
115
|
+
|
116
|
+
>> @presenter.end_date
|
117
|
+
=> Thu, 31 Dec 2009
|
118
|
+
|
119
|
+
>> # User selects some other month and year; the next request looks like...
|
120
|
+
|
121
|
+
>> @presenter = CalendarPresenter.new({:month => '2', :year => '2002'})
|
122
|
+
|
123
|
+
>> @presenter.start_date
|
124
|
+
=> Fri, 01 Feb 2002
|
125
|
+
|
126
|
+
>> @presenter.end_date
|
127
|
+
=> Thu, 28 Feb 2002
|
128
|
+
|
129
|
+
...
|
130
|
+
|
131
|
+
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.
|
132
|
+
|
133
|
+
_you can access the attributes via the attributes hash. Only default and specified attributes will have entries here._
|
134
|
+
|
135
|
+
class Person < Valuable
|
136
|
+
has_value :name
|
137
|
+
has_value :is_developer, :default => false
|
138
|
+
has_value :ssn
|
139
|
+
end
|
140
|
+
|
141
|
+
>> elvis = Person.new(:name => 'The King')
|
142
|
+
|
143
|
+
>> elvis.attributes
|
144
|
+
=> {:name=>"The King", :is_developer=>false}
|
145
|
+
|
146
|
+
>> elvis.attributes[:name]
|
147
|
+
=> "The King"
|
148
|
+
|
149
|
+
>> elvis.ssn
|
150
|
+
=> nil
|
151
|
+
|
152
|
+
_also, you can get a list of all the defined attributes from the class_
|
153
|
+
|
154
|
+
>> Person.attributes
|
155
|
+
=> [:name, :is_developer, :ssn]
|
156
|
+
|
157
|
+
Default Values
|
158
|
+
--------------
|
159
|
+
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.
|
160
|
+
|
161
|
+
When a default value and a klass are specified, the default value will NOT be cast to type klass -- you must do it.
|
162
|
+
|
163
|
+
If a value having a default is set to null after it is constructed, it will NOT be set to the default.
|
164
|
+
|
165
|
+
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.
|
166
|
+
|
167
|
+
KLASS-ification
|
168
|
+
---------------
|
169
|
+
`:integer`, `:string` and `:boolean` use `to_i`, `to_s` and `!!` respectively. All other klasses use `klass.new(value)` unless the value `is_a?(klass)`, in which case it is unmolested. Nils are never klassified. In the example above, hits, which is an integer, is `nil` if not set, rather than `nil.to_i = 0`.
|
data/lib/valuable.rb
ADDED
@@ -0,0 +1,233 @@
|
|
1
|
+
# Valuable is the class from which all classes (who are so inclined)
|
2
|
+
# should inherit.
|
3
|
+
#
|
4
|
+
# ==Example:
|
5
|
+
#
|
6
|
+
# class Bus < Valuable
|
7
|
+
#
|
8
|
+
# has_value :number, :klass => :integer
|
9
|
+
# has_value :color, :default => 'yellow'
|
10
|
+
# has_collection :riders
|
11
|
+
#
|
12
|
+
# end
|
13
|
+
#
|
14
|
+
# >> Bus.attributes
|
15
|
+
# => [:number, :color, :riders]
|
16
|
+
# >> bus = Bus.new(:number => '3', :riders => ['GOF', 'Fowler', 'Mort']
|
17
|
+
# >> bus.attributes
|
18
|
+
# => {:number => 3, :riders => ['GOF', 'Fowler', 'Mort'], :color => 'yellow'}
|
19
|
+
class Valuable
|
20
|
+
|
21
|
+
# Returns a Hash representing all known values. Values are set three ways:
|
22
|
+
#
|
23
|
+
# (1) a default value
|
24
|
+
# (2) they were passed to the constructor
|
25
|
+
# Bus.new(:color => 'green')
|
26
|
+
# (3) they were set via their namesake setter
|
27
|
+
# bus.color = 'green'
|
28
|
+
#
|
29
|
+
# Values that have not been set and have no default not appear in this
|
30
|
+
# collection. Their namesake attribute methods will respond with nil.
|
31
|
+
# Always use symbols to access these values.
|
32
|
+
#
|
33
|
+
# >> bus = Bus.new(:number => 16) # color has default value 'yellow'
|
34
|
+
# >> bus.attributes
|
35
|
+
# => {:color => 'yellow', :number => 16}
|
36
|
+
def attributes
|
37
|
+
@attributes ||= deep_duplicate_of(self.class.defaults)
|
38
|
+
end
|
39
|
+
|
40
|
+
# accepts an optional hash that will be used to populate the
|
41
|
+
# predefined attributes for this class.
|
42
|
+
def initialize(atts = nil)
|
43
|
+
atts.each { |name, value| __send__("#{name}=", value ) } if atts
|
44
|
+
end
|
45
|
+
|
46
|
+
def deep_duplicate_of(value)
|
47
|
+
Marshal.load(Marshal.dump(value))
|
48
|
+
end
|
49
|
+
|
50
|
+
class << self
|
51
|
+
|
52
|
+
# Returns an array of the attributes available on this object.
|
53
|
+
def attributes
|
54
|
+
@attributes ||= []
|
55
|
+
end
|
56
|
+
|
57
|
+
# Returns a name/value set of the values that will be used on
|
58
|
+
# instanciation unless new values are provided.
|
59
|
+
#
|
60
|
+
# >> Bus.defaults
|
61
|
+
# => {:color => 'yellow'}
|
62
|
+
def defaults
|
63
|
+
@defaults ||= {}
|
64
|
+
end
|
65
|
+
|
66
|
+
# Decorator method that lets you specify the attributes for your
|
67
|
+
# model. It accepts an attribute name (a symbol) and an options
|
68
|
+
# hash. Valid options are :default, :klass and (when :klass is
|
69
|
+
# Boolean) :negative.
|
70
|
+
#
|
71
|
+
# :default - for the given attribute, use this value if no other
|
72
|
+
# is provided.
|
73
|
+
#
|
74
|
+
# :klass - light weight type casting. Use :integer, :string or
|
75
|
+
# :boolean. Alternately, supply a class.
|
76
|
+
#
|
77
|
+
# When a :klassified attribute is set to some new value, if the value
|
78
|
+
# is not nil and is not already of that class, the value will be cast
|
79
|
+
# to the specified klass. In the case of :integer, it wil be done via
|
80
|
+
# .to_i. In the case of a random other class, it will be done via
|
81
|
+
# Class.new(value). If the value is nil, it will not be cast.
|
82
|
+
#
|
83
|
+
# A good example: PhoneNumber < String is useful if you
|
84
|
+
# want numbers to come out the other end properly formatted, when your
|
85
|
+
# input may come in as an integer, or string without formatting, or
|
86
|
+
# string with bad formatting.
|
87
|
+
#
|
88
|
+
# IMPORTANT EXCEPTION
|
89
|
+
#
|
90
|
+
# Due to the way Rails handles checkboxes, '0' resolves to FALSE,
|
91
|
+
# though it would normally resolve to TRUE.
|
92
|
+
def has_value(name, options={})
|
93
|
+
|
94
|
+
name = name.to_sym
|
95
|
+
|
96
|
+
attributes << name
|
97
|
+
|
98
|
+
defaults[name] = options[:default] unless options[:default].nil?
|
99
|
+
|
100
|
+
create_accessor_for(name)
|
101
|
+
create_question_for(name) if options[:klass] == :boolean
|
102
|
+
create_negative_question_for(name, options[:negative]) if options[:klass] == :boolean && options[:negative]
|
103
|
+
|
104
|
+
create_setter_for(name, options[:klass], options[:default])
|
105
|
+
|
106
|
+
check_options_validity(name, options)
|
107
|
+
end
|
108
|
+
|
109
|
+
# Creates the method that sets the value of an attribute. This setter
|
110
|
+
# is called both by the constructor. The constructor handles type
|
111
|
+
# casting. Setting values via the attributes hash avoids the method
|
112
|
+
# defined here.
|
113
|
+
def create_setter_for(name, klass, default)
|
114
|
+
|
115
|
+
case klass
|
116
|
+
when NilClass
|
117
|
+
|
118
|
+
define_method "#{name}=" do |value|
|
119
|
+
attributes[name] = value
|
120
|
+
end
|
121
|
+
|
122
|
+
when :integer
|
123
|
+
|
124
|
+
define_method "#{name}=" do |value|
|
125
|
+
value_as_integer = value && value.to_i
|
126
|
+
attributes[name] = value_as_integer
|
127
|
+
end
|
128
|
+
|
129
|
+
when :string
|
130
|
+
|
131
|
+
define_method "#{name}=" do |value|
|
132
|
+
value_as_string = value && value.to_s
|
133
|
+
attributes[name] = value_as_string
|
134
|
+
end
|
135
|
+
|
136
|
+
when :boolean
|
137
|
+
|
138
|
+
define_method "#{name}=" do |value|
|
139
|
+
attributes[name] = value == '0' ? false : !!value
|
140
|
+
end
|
141
|
+
|
142
|
+
else
|
143
|
+
|
144
|
+
define_method "#{name}=" do |value|
|
145
|
+
if value.nil?
|
146
|
+
attributes[name] = nil
|
147
|
+
elsif value.is_a? klass
|
148
|
+
attributes[name] = value
|
149
|
+
else
|
150
|
+
attributes[name] = klass.new(value)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
# creates a simple accessor method named after the attribute whose
|
157
|
+
# value it will provide during the life of the instance.
|
158
|
+
def create_accessor_for(name)
|
159
|
+
define_method name do
|
160
|
+
attributes[name]
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
# In addition to the normal getter and setter, boolean attributes
|
165
|
+
# get a method appended with a ?.
|
166
|
+
#
|
167
|
+
# class Player < Valuable
|
168
|
+
# has_value :free_agent, :klass => Boolean
|
169
|
+
# end
|
170
|
+
#
|
171
|
+
# juan = Player.new(:free_agent => true)
|
172
|
+
# >> juan.free_agent?
|
173
|
+
# => true
|
174
|
+
def create_question_for(name)
|
175
|
+
define_method "#{name}?" do
|
176
|
+
attributes[name]
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
# In some situations, the opposite of a value may be just as interesting.
|
181
|
+
#
|
182
|
+
# class Coder < Valuable
|
183
|
+
# has_value :agilist, :klass => Boolean, :negative => :waterfaller
|
184
|
+
# end
|
185
|
+
#
|
186
|
+
# monkey = Coder.new(:agilist => false)
|
187
|
+
# >> monkey.waterfaller?
|
188
|
+
# => true
|
189
|
+
def create_negative_question_for(name, negative)
|
190
|
+
define_method "#{negative}?" do
|
191
|
+
!attributes[name]
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
# this is a more intuitive way of marking an attribute as holding a
|
196
|
+
# collection.
|
197
|
+
#
|
198
|
+
# class Bus < Valuable
|
199
|
+
# has_value :riders, :default => [] # meh...
|
200
|
+
# has_collection :riders # better!
|
201
|
+
# end
|
202
|
+
#
|
203
|
+
# >> bus = Bus.new
|
204
|
+
# >> bus.riders << 'jack'
|
205
|
+
# >> bus.riders
|
206
|
+
# => ['jack']
|
207
|
+
def has_collection(name)
|
208
|
+
has_value(name, :default => [] )
|
209
|
+
end
|
210
|
+
|
211
|
+
private
|
212
|
+
|
213
|
+
def inherited(child)
|
214
|
+
attributes.each {|att| child.attributes << att }
|
215
|
+
defaults.each {|(name, value)| child.defaults[name] = value }
|
216
|
+
end
|
217
|
+
|
218
|
+
def known_options
|
219
|
+
[:klass, :default, :negative]
|
220
|
+
end
|
221
|
+
|
222
|
+
# this helper raises an exception if the options passed to has_value
|
223
|
+
# are wrong. Mostly written because I occasionally used :class instead
|
224
|
+
# of :klass and, being a moron, wasted time trying to find the issue.
|
225
|
+
def check_options_validity(name, options)
|
226
|
+
invalid_options = options.keys - known_options
|
227
|
+
|
228
|
+
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?
|
229
|
+
end
|
230
|
+
|
231
|
+
end
|
232
|
+
|
233
|
+
end
|
data/rakefile.rb
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'config.rb'
|
3
|
+
|
4
|
+
require 'rake'
|
5
|
+
require 'rake/rdoctask'
|
6
|
+
require 'rake/testtask'
|
7
|
+
require 'rake/packagetask'
|
8
|
+
require 'rake/gempackagetask'
|
9
|
+
require 'rake/contrib/rubyforgepublisher'
|
10
|
+
|
11
|
+
task :default => [:test]
|
12
|
+
|
13
|
+
PKG_FILE_NAME = "#{CONFIG[:name]}-#{CONFIG[:version]}"
|
14
|
+
RUBY_FORGE_PROJECT = 'valuable'
|
15
|
+
RUBY_FORGE_USER = 'mustmodify'
|
16
|
+
|
17
|
+
desc "Run unit tests"
|
18
|
+
Rake::TestTask.new("test") { |t|
|
19
|
+
t.pattern = 'test/*_test.rb'
|
20
|
+
t.verbose = true
|
21
|
+
t.warning = true
|
22
|
+
}
|
23
|
+
|
24
|
+
desc 'clean temporary files, rdoc, and gem package'
|
25
|
+
task :clean => [:clobber_package, :clobber_rdoc] do
|
26
|
+
temp_filenames = File.join('**', '*.*~')
|
27
|
+
temp_files = Dir.glob(temp_filenames)
|
28
|
+
if temp_files.empty?
|
29
|
+
puts 'no temp files to delete'
|
30
|
+
else
|
31
|
+
puts "deleting #{temp_files.size} temp files"
|
32
|
+
end
|
33
|
+
|
34
|
+
File.delete(*temp_files)
|
35
|
+
end
|
36
|
+
|
37
|
+
desc 'Generate documentation for the Valuable plugin'
|
38
|
+
Rake::RDocTask.new(:rdoc) do |rdoc|
|
39
|
+
rdoc.title = 'Valuable - light weight modeling'
|
40
|
+
rdoc.options << '--line-numbers'
|
41
|
+
rdoc.options << '--inline-source'
|
42
|
+
rdoc.rdoc_files.include('README.markdown')
|
43
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
44
|
+
end
|
45
|
+
|
46
|
+
spec = Gem::Specification.new do |s|
|
47
|
+
s.name = CONFIG[:name]
|
48
|
+
s.version = CONFIG[:version]
|
49
|
+
s.platform = Gem::Platform::RUBY
|
50
|
+
s.summary = 'attr_accessor on steroids with defaults, constructor, and light casting.'
|
51
|
+
s.description = "Valuable is a ruby base class that is essentially attr_accessor on steroids. It intends to use a simple and intuitive interface, allowing you to get on with the logic specific to your application."
|
52
|
+
|
53
|
+
s.files = FileList["{lib, test, examples}/**/*"].to_a + %w( README.markdown rakefile.rb )
|
54
|
+
s.require_path = 'lib'
|
55
|
+
s.test_files = FileList["{test}/**/*test.rb"].to_a
|
56
|
+
s.has_rdoc = true
|
57
|
+
|
58
|
+
s.rubyforge_project = RUBY_FORGE_PROJECT
|
59
|
+
s.author = 'Johnathon Wright'
|
60
|
+
s.email = 'jw@mustmodify.com'
|
61
|
+
s.homepage = 'http://www.github.com/mustmodify/valuable'
|
62
|
+
end
|
63
|
+
|
64
|
+
Rake::GemPackageTask.new(spec) do |pkg|
|
65
|
+
#pkg.need_zip = true
|
66
|
+
#pkg.need_tar = true
|
67
|
+
end
|
68
|
+
|
69
|
+
desc "Publish the API documentation"
|
70
|
+
task :pdoc => [:rdoc] do
|
71
|
+
Rake::RubyForgePublisher.new(RUBY_FORGE_PROJECT, RUBY_FORGE_USER).upload
|
72
|
+
end
|
73
|
+
|
74
|
+
desc 'Publish the gem and API docs'
|
75
|
+
task :publish => [:pdoc, :rubyforge_upload]
|
76
|
+
|
@@ -0,0 +1,23 @@
|
|
1
|
+
$: << File.expand_path(File.dirname(__FILE__) + '/../lib')
|
2
|
+
|
3
|
+
require 'test/unit'
|
4
|
+
require 'valuable.rb'
|
5
|
+
require 'mocha'
|
6
|
+
|
7
|
+
class Infrastructure < Valuable
|
8
|
+
end
|
9
|
+
|
10
|
+
class BadAttributesTest < Test::Unit::TestCase
|
11
|
+
|
12
|
+
def test_that_has_value_grumbles_when_it_gets_bad_attributes
|
13
|
+
assert_raises ArgumentError do
|
14
|
+
Infrastructure.has_value :fu, :invalid => 'shut your mouth'
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def test_that_valid_arguments_cause_no_grumbling
|
19
|
+
assert_nothing_raised do
|
20
|
+
Infrastructure.has_value :bar, :klass => Integer
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
$: << File.expand_path(File.dirname(__FILE__) + '/../lib')
|
2
|
+
|
3
|
+
require 'test/unit'
|
4
|
+
require 'valuable.rb'
|
5
|
+
require 'mocha'
|
6
|
+
|
7
|
+
class Signature < String
|
8
|
+
end
|
9
|
+
|
10
|
+
class Cube < String
|
11
|
+
def initialize(number)
|
12
|
+
super "Lives in Cube #{number}"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class DevCertifications < Valuable
|
17
|
+
has_value :a_plus, :default => false
|
18
|
+
has_value :mcts, :default => false
|
19
|
+
has_value :hash_rocket, :default => false
|
20
|
+
end
|
21
|
+
|
22
|
+
class Dev < Valuable
|
23
|
+
has_value :has_exposure_to_sunlight, :default => false
|
24
|
+
has_value :mindset
|
25
|
+
has_value :name, :default => 'DHH Jr.', :klass => String
|
26
|
+
has_value :signature, :klass => Signature
|
27
|
+
has_value :cubical, :klass => Cube
|
28
|
+
has_value :hacker, :default => true
|
29
|
+
has_value :certifications, :default => DevCertifications.new
|
30
|
+
has_value :quote
|
31
|
+
|
32
|
+
has_collection :favorite_gems
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
# Previously, we used :klass => Klass instead of :klass => :klass.
|
37
|
+
# I decided it was just plain dirty. On refactoring, I realized that
|
38
|
+
# most it would continue to work. Other stuff, unfortunately, would
|
39
|
+
# break horribly. (Integer.new, for instance, makes Ruby very angry.)
|
40
|
+
# The purpose of these tests is to verify that everything _either_
|
41
|
+
# breaks horribly or works, where the third option is fails silently
|
42
|
+
# and mysteriously.
|
43
|
+
class DeprecatedTest < Test::Unit::TestCase
|
44
|
+
|
45
|
+
def test_that_attributes_can_be_klassified
|
46
|
+
dev = Dev.new(:signature => 'brah brah')
|
47
|
+
assert_equal Signature, dev.signature.class
|
48
|
+
end
|
49
|
+
|
50
|
+
def test_that_randomly_classed_attributes_persist_nils
|
51
|
+
assert_equal nil, Dev.new.signature
|
52
|
+
end
|
53
|
+
|
54
|
+
def test_that_randomly_classed_attributes_respect_defaults
|
55
|
+
assert_equal 'DHH Jr.', Dev.new.name
|
56
|
+
end
|
57
|
+
|
58
|
+
def test_that_constructor_casts_attributes
|
59
|
+
assert_equal 'Lives in Cube 20', Dev.new(:cubical => 20).cubical
|
60
|
+
end
|
61
|
+
|
62
|
+
def test_that_setter_casts_attributes
|
63
|
+
golden_boy = Dev.new
|
64
|
+
golden_boy.cubical = 20
|
65
|
+
|
66
|
+
assert_equal 'Lives in Cube 20', golden_boy.cubical
|
67
|
+
end
|
68
|
+
|
69
|
+
def test_that_properly_klassed_values_are_not_rekast
|
70
|
+
why_hammer = Signature.new('go ask your mom')
|
71
|
+
Signature.expects(:new).with(why_hammer).never
|
72
|
+
hammer = Dev.new(:signature => why_hammer)
|
73
|
+
end
|
74
|
+
|
75
|
+
def test_that_default_values_can_be_set_to_nothing
|
76
|
+
assert_equal nil, Dev.new(:hacker => nil).hacker
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
|
@@ -0,0 +1,28 @@
|
|
1
|
+
$: << File.expand_path(File.dirname(__FILE__) + '/../lib')
|
2
|
+
|
3
|
+
require 'test/unit'
|
4
|
+
require 'valuable.rb'
|
5
|
+
require 'mocha'
|
6
|
+
|
7
|
+
class Parent < Valuable
|
8
|
+
has_value :name, :default => 'unknown'
|
9
|
+
end
|
10
|
+
|
11
|
+
class Child < Parent
|
12
|
+
has_value :age
|
13
|
+
end
|
14
|
+
|
15
|
+
class InheritanceTest < Test::Unit::TestCase
|
16
|
+
|
17
|
+
def test_that_children_inherit_their_parents_attributes
|
18
|
+
assert Child.attributes.include?(:name)
|
19
|
+
end
|
20
|
+
|
21
|
+
def test_that_children_have_distinctive_attributes
|
22
|
+
assert Child.attributes.include?(:age)
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_that_parents_do_not_inherit_things_from_children
|
26
|
+
assert_equal [:name], Parent.attributes
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,194 @@
|
|
1
|
+
$: << File.expand_path(File.dirname(__FILE__) + '/../lib')
|
2
|
+
|
3
|
+
require 'test/unit'
|
4
|
+
require 'valuable.rb'
|
5
|
+
require 'mocha'
|
6
|
+
|
7
|
+
class Cubical < String
|
8
|
+
def initialize(number)
|
9
|
+
super "Lives in Cubical #{number}"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class DevCertifications < Valuable
|
14
|
+
has_value :a_plus, :default => false
|
15
|
+
has_value :mcts, :default => false
|
16
|
+
has_value :hash_rocket, :default => false
|
17
|
+
end
|
18
|
+
|
19
|
+
class Developer < Valuable
|
20
|
+
has_value :experience, :klass => :integer
|
21
|
+
has_value :has_exposure_to_sunlight, :default => false
|
22
|
+
has_value :mindset
|
23
|
+
has_value :name, :default => 'DHH Jr.', :klass => :string
|
24
|
+
has_value :snacks_per_day, :klass => :integer, :default => 7
|
25
|
+
has_value :cubical, :klass => Cubical
|
26
|
+
has_value :hacker, :default => true
|
27
|
+
has_value :certifications, :klass => DevCertifications, :default => DevCertifications.new
|
28
|
+
has_value :quote
|
29
|
+
has_value :employed, :klass => :boolean, :negative => 'unemployed'
|
30
|
+
|
31
|
+
has_collection :favorite_gems
|
32
|
+
|
33
|
+
end
|
34
|
+
|
35
|
+
class BaseTest < Test::Unit::TestCase
|
36
|
+
|
37
|
+
def test_that_an_attributes_hash_is_available
|
38
|
+
assert_kind_of(Hash, Developer.new.attributes)
|
39
|
+
end
|
40
|
+
|
41
|
+
def test_that_static_defaults_hash_is_available
|
42
|
+
assert_equal 'DHH Jr.', Developer.defaults[:name]
|
43
|
+
end
|
44
|
+
|
45
|
+
def test_that_an_accessor_is_created
|
46
|
+
dev = Developer.new(:mindset => :agile)
|
47
|
+
assert_equal :agile, dev.mindset
|
48
|
+
end
|
49
|
+
|
50
|
+
def test_that_setter_is_created
|
51
|
+
dev = Developer.new
|
52
|
+
dev.mindset = :enterprisey
|
53
|
+
assert_equal :enterprisey, dev.mindset
|
54
|
+
end
|
55
|
+
|
56
|
+
def test_that_attributes_can_be_cast_as_integer
|
57
|
+
dev = Developer.new(:experience => 9.2)
|
58
|
+
assert_equal 9, dev.experience
|
59
|
+
end
|
60
|
+
|
61
|
+
def test_that_integer_attributes_respect_default
|
62
|
+
assert_equal 7, Developer.new.snacks_per_day
|
63
|
+
end
|
64
|
+
|
65
|
+
def test_that_an_integer_attribute_with_no_value_results_in_nil
|
66
|
+
assert_equal nil, Developer.new.experience
|
67
|
+
end
|
68
|
+
|
69
|
+
def test_that_attributes_can_be_klassified
|
70
|
+
dev = Developer.new(:cubical => 12)
|
71
|
+
assert_equal Cubical, dev.cubical.class
|
72
|
+
end
|
73
|
+
|
74
|
+
def test_that_defaults_appear_in_attributes_hash
|
75
|
+
assert_equal false, Developer.new.attributes[:has_exposure_to_sunlight]
|
76
|
+
end
|
77
|
+
|
78
|
+
def test_that_attributes_can_have_default_values
|
79
|
+
assert_equal false, Developer.new.has_exposure_to_sunlight
|
80
|
+
end
|
81
|
+
|
82
|
+
def test_that_randomly_classed_attributes_persist_nils
|
83
|
+
assert_equal nil, Developer.new.cubical
|
84
|
+
end
|
85
|
+
|
86
|
+
def test_that_randomly_classed_attributes_respect_defaults
|
87
|
+
assert_equal 'DHH Jr.', Developer.new.name
|
88
|
+
end
|
89
|
+
|
90
|
+
def test_that_constructor_casts_attributes
|
91
|
+
assert_equal 'Lives in Cubical 20', Developer.new(:cubical => 20).cubical
|
92
|
+
end
|
93
|
+
|
94
|
+
def test_that_setter_casts_attributes
|
95
|
+
golden_boy = Developer.new
|
96
|
+
golden_boy.cubical = 20
|
97
|
+
|
98
|
+
assert_equal 'Lives in Cubical 20', golden_boy.cubical
|
99
|
+
end
|
100
|
+
|
101
|
+
def test_that_attributes_are_available_as_class_method
|
102
|
+
assert Developer.attributes.include?(:cubical)
|
103
|
+
end
|
104
|
+
|
105
|
+
def test_that_a_model_can_have_a_collection
|
106
|
+
assert_equal [], Developer.new.favorite_gems
|
107
|
+
end
|
108
|
+
|
109
|
+
def test_that_values_do_not_mysteriously_jump_instances
|
110
|
+
panda = Developer.new
|
111
|
+
panda.mindset = 'geek'
|
112
|
+
|
113
|
+
hammer = Developer.new
|
114
|
+
|
115
|
+
assert_not_equal 'geek', hammer.mindset
|
116
|
+
end
|
117
|
+
|
118
|
+
def test_that_collection_values_do_not_roll_across_instances
|
119
|
+
jim = Developer.new
|
120
|
+
jim.favorite_gems << 'Ruby'
|
121
|
+
|
122
|
+
clark = Developer.new
|
123
|
+
|
124
|
+
assert_equal [], clark.favorite_gems
|
125
|
+
end
|
126
|
+
|
127
|
+
def test_that_attributes_are_cast
|
128
|
+
panda = Developer.new(:name => 'Code Panda', :experience => '8')
|
129
|
+
assert_kind_of Integer, panda.attributes[:experience]
|
130
|
+
end
|
131
|
+
|
132
|
+
def test_that_stringy_keys_are_tried_in_absence_of_symbolic_keys
|
133
|
+
homer = Developer.new('quote' => "D'oh!")
|
134
|
+
assert_equal "D'oh!", homer.quote
|
135
|
+
end
|
136
|
+
|
137
|
+
def test_that_default_values_from_seperate_instances_are_not_references_to_the_default_value_for_that_field
|
138
|
+
assert_not_equal Developer.new.favorite_gems.object_id, Developer.new.favorite_gems.object_id
|
139
|
+
end
|
140
|
+
|
141
|
+
def test_that_properly_klassed_values_are_not_rekast
|
142
|
+
stapler = Cubical.new('in sub-basement')
|
143
|
+
Cubical.expects(:new).with(stapler).never
|
144
|
+
Developer.new(:cubical => stapler)
|
145
|
+
end
|
146
|
+
|
147
|
+
def test_that_values_can_be_set_to_false
|
148
|
+
assert_equal false, Developer.new(:hacker => false).hacker
|
149
|
+
end
|
150
|
+
|
151
|
+
def test_that_default_values_needing_deep_duplication_get_it
|
152
|
+
a = Developer.new
|
153
|
+
b = Developer.new
|
154
|
+
|
155
|
+
a.certifications.hash_rocket = true
|
156
|
+
assert_equal false, b.certifications.hash_rocket
|
157
|
+
end
|
158
|
+
|
159
|
+
def test_that_default_values_can_be_set_to_nothing
|
160
|
+
assert_equal nil, Developer.new(:hacker => nil).hacker
|
161
|
+
end
|
162
|
+
|
163
|
+
def test_that_values_are_cast_to_boolean
|
164
|
+
assert_equal false, Developer.new(:employed => nil).employed
|
165
|
+
end
|
166
|
+
|
167
|
+
def test_that_string_zero_becomes_false
|
168
|
+
assert_equal false, Developer.new(:employed => '0').employed
|
169
|
+
end
|
170
|
+
|
171
|
+
def test_that_boolean_values_get_questionmarked_methods
|
172
|
+
assert Developer.instance_methods.include?('employed?')
|
173
|
+
end
|
174
|
+
|
175
|
+
def test_that_boolean_values_get_negative_methods
|
176
|
+
assert Developer.instance_methods.include?('unemployed?')
|
177
|
+
end
|
178
|
+
|
179
|
+
def test_that_negative_methods_are_negative
|
180
|
+
assert_equal true, Developer.new(:employed => false).unemployed?
|
181
|
+
end
|
182
|
+
|
183
|
+
def test_that_constructor_can_handle_an_instance_of_nothing
|
184
|
+
assert_nothing_raised do
|
185
|
+
Developer.new(nil)
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
def test_that_klassification_does_not_break_when_stringified
|
190
|
+
assert_nothing_raised do
|
191
|
+
Developer.new(:experience => '2')
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
metadata
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: Valuable
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: "0.8"
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Johnathon Wright
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-12-09 00:00:00 -06:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description: Valuable is a ruby base class that is essentially attr_accessor on steroids. It intends to use a simple and intuitive interface, allowing you to get on with the logic specific to your application.
|
17
|
+
email: jw@mustmodify.com
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files: []
|
23
|
+
|
24
|
+
files:
|
25
|
+
- lib/valuable.rb
|
26
|
+
- README.markdown
|
27
|
+
- rakefile.rb
|
28
|
+
has_rdoc: true
|
29
|
+
homepage: http://www.github.com/mustmodify/valuable
|
30
|
+
licenses: []
|
31
|
+
|
32
|
+
post_install_message:
|
33
|
+
rdoc_options: []
|
34
|
+
|
35
|
+
require_paths:
|
36
|
+
- lib
|
37
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - ">="
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: "0"
|
42
|
+
version:
|
43
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: "0"
|
48
|
+
version:
|
49
|
+
requirements: []
|
50
|
+
|
51
|
+
rubyforge_project: valuable
|
52
|
+
rubygems_version: 1.3.5
|
53
|
+
signing_key:
|
54
|
+
specification_version: 3
|
55
|
+
summary: attr_accessor on steroids with defaults, constructor, and light casting.
|
56
|
+
test_files:
|
57
|
+
- test/bad_attributes_test.rb
|
58
|
+
- test/deprecated_test.rb
|
59
|
+
- test/inheritance_test.rb
|
60
|
+
- test/valuable_test.rb
|