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