valuable 0.6
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.txt +79 -0
- data/lib/boolean.rb +2 -0
- data/lib/valuable.rb +208 -0
- data/rakefile.rb +193 -0
- data/test/bad_attributes_test.rb +23 -0
- data/test/valuable_test.rb +182 -0
- metadata +68 -0
data/README.txt
ADDED
@@ -0,0 +1,79 @@
|
|
1
|
+
|
2
|
+
=Introducing Valuable
|
3
|
+
|
4
|
+
Valuable is a ruby base class that is essentially attr_accessor on steroids. Its aim is to provide Rails-like goodness without the database. 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.
|
5
|
+
|
6
|
+
Valuable provides DRY decoration like attr_accessor, but includes default values, light weight type casting, a constructor that accepts an attributes hash, a class-level list of attributes, an instance-level attributes hash, and more.
|
7
|
+
|
8
|
+
==Example
|
9
|
+
|
10
|
+
class BaseballPlayer < Valuable
|
11
|
+
|
12
|
+
has_value :at_bats, :klass => Integer
|
13
|
+
has_value :hits, :klass => Integer
|
14
|
+
has_value :league, :default => 'unknown'
|
15
|
+
has_value :name, :dependency => true
|
16
|
+
has_value :cell, :klass => PhoneNumber, :default => 'Unknown'
|
17
|
+
has_value :active, :klass => Boolean
|
18
|
+
|
19
|
+
has_collection :teammates
|
20
|
+
|
21
|
+
def average
|
22
|
+
hits/at_bats.to_f if hits && at_bats
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
|
27
|
+
joe = BaseballPlayer.new(:name => 'Joe', :hits => 5, :at_bats => 20, :cell => '1234567890')
|
28
|
+
|
29
|
+
joe.at_bats
|
30
|
+
|
31
|
+
20
|
32
|
+
|
33
|
+
joe.active?
|
34
|
+
|
35
|
+
nil
|
36
|
+
|
37
|
+
joe.league
|
38
|
+
|
39
|
+
'unknown'
|
40
|
+
|
41
|
+
joe.average
|
42
|
+
|
43
|
+
0.25
|
44
|
+
|
45
|
+
joe.at_bats = nil
|
46
|
+
|
47
|
+
joe.average
|
48
|
+
|
49
|
+
nil
|
50
|
+
|
51
|
+
joe.teammates
|
52
|
+
|
53
|
+
[]
|
54
|
+
|
55
|
+
joe.cell
|
56
|
+
|
57
|
+
'(123) 456-7890'
|
58
|
+
|
59
|
+
joe.cell = nil
|
60
|
+
|
61
|
+
joe.cell
|
62
|
+
|
63
|
+
nil
|
64
|
+
|
65
|
+
==DEFAULT VALUES
|
66
|
+
|
67
|
+
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.
|
68
|
+
|
69
|
+
When a default value and a klass are specified, the default value will NOT be cast to type klass -- you must do it. See "jersey" example above.
|
70
|
+
|
71
|
+
If a value having a default is set to null after it is constructed, it will NOT be set to the default.
|
72
|
+
|
73
|
+
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.
|
74
|
+
|
75
|
+
==KLASS-ification
|
76
|
+
|
77
|
+
Boolean is defined as a module in lib for uniformity.
|
78
|
+
|
79
|
+
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/boolean.rb
ADDED
data/lib/valuable.rb
ADDED
@@ -0,0 +1,208 @@
|
|
1
|
+
require 'active_support'
|
2
|
+
require File.dirname(__FILE__) + '/boolean.rb'
|
3
|
+
# This is the base class from which all classes that intend to use
|
4
|
+
# Valuable should inherit.
|
5
|
+
#
|
6
|
+
# Example:
|
7
|
+
#
|
8
|
+
# class Bus < Valuable
|
9
|
+
# has_value :color, :default => 'yellow'
|
10
|
+
# has_value :number, :klass => Integer
|
11
|
+
# has_collection :riders
|
12
|
+
# end
|
13
|
+
class Valuable
|
14
|
+
|
15
|
+
# Returns a Hash with all the name value pairs that have values so far,
|
16
|
+
# either because they had a default value or because they were set in
|
17
|
+
# the constructor or after instanciation. Values that have not been defined
|
18
|
+
# and which have no default value will not appear in this collection.
|
19
|
+
#
|
20
|
+
# Currently returns a HashWithIndifferentAccess, though that may change
|
21
|
+
# since it would remove the dependancy on ActiveSupport. Always use symbols
|
22
|
+
# to access these values.
|
23
|
+
#
|
24
|
+
# >> bus = Bus.new(:number => 16) # color has default value 'yellow'
|
25
|
+
# >> bus.attributes
|
26
|
+
# => {:color => 'yellow', :number => 16}
|
27
|
+
def attributes
|
28
|
+
@attributes ||= HashWithIndifferentAccess.new(deep_duplicate_of(self.class.defaults))
|
29
|
+
end
|
30
|
+
|
31
|
+
# accepts a hash that will be used to populate the predefined attributes
|
32
|
+
# for this class.
|
33
|
+
def initialize(atts = {})
|
34
|
+
atts.each { |name, value| __send__("#{name}=", value ) }
|
35
|
+
end
|
36
|
+
|
37
|
+
# Creates a duplicate of all values.
|
38
|
+
def deep_duplicate_of(value)
|
39
|
+
Marshal.load(Marshal.dump(value))
|
40
|
+
end
|
41
|
+
|
42
|
+
class << self
|
43
|
+
|
44
|
+
# Returns an array of the attributes available on this object.
|
45
|
+
def attributes
|
46
|
+
@attributes ||= []
|
47
|
+
end
|
48
|
+
|
49
|
+
# Returns a name/value set of the values that will be used on
|
50
|
+
# instanciation unless new values are provided.
|
51
|
+
#
|
52
|
+
# class Bus < Valuable
|
53
|
+
# has_value :color, :default => 'yellow'
|
54
|
+
# end
|
55
|
+
#
|
56
|
+
# >> Bus.defaults
|
57
|
+
# => {:color => 'yellow'}
|
58
|
+
#
|
59
|
+
def defaults
|
60
|
+
@defaults ||= {}
|
61
|
+
end
|
62
|
+
|
63
|
+
# Decorator method that determines which attributes will be available
|
64
|
+
# for each instance of the object being created. It accepts an
|
65
|
+
# attribute name (a symbol) and an options hash. Valid options are
|
66
|
+
# :default, :klass and (when :klass is Boolean) :negative.
|
67
|
+
#
|
68
|
+
# :default - for the given attribute, use this value if no other is
|
69
|
+
# provided.
|
70
|
+
#
|
71
|
+
# :klass - light weight type casting. Use Integer, String, Boolean, or
|
72
|
+
# related classes. Alternately, supply a class. When that attribute is
|
73
|
+
# set, if the value isn't already of that class, the result will be
|
74
|
+
# an instance of that class, with the value passed to the constructory.
|
75
|
+
#
|
76
|
+
# A great example: PhoneNumber < String is useful if you
|
77
|
+
# want numbers to come out the other end properly formatted, when your
|
78
|
+
# input may come in as an integer, or string without formatting, or
|
79
|
+
# string with bad formatting.
|
80
|
+
def has_value(name, options={})
|
81
|
+
attributes << name
|
82
|
+
|
83
|
+
defaults[name] = options[:default] unless options[:default].nil?
|
84
|
+
|
85
|
+
create_accessor_for(name)
|
86
|
+
create_question_for(name) if options[:klass] == Boolean
|
87
|
+
create_negative_question_for(name, options[:negative]) if options[:klass] == Boolean && options[:negative]
|
88
|
+
|
89
|
+
create_setter_for(name, options[:klass], options[:default])
|
90
|
+
|
91
|
+
check_options_validity(name, options)
|
92
|
+
end
|
93
|
+
|
94
|
+
# This method returns an array of symbols, which are the only allowed
|
95
|
+
# options for has_value.
|
96
|
+
def known_options
|
97
|
+
[:klass, :default, :negative]
|
98
|
+
end
|
99
|
+
|
100
|
+
# validates option hashes passed to has_value
|
101
|
+
def check_options_validity(name, options)
|
102
|
+
invalid_options = options.keys - known_options
|
103
|
+
|
104
|
+
raise ArgumentError, "has_value did not know how to respond to #{invalid_options.join(', ')}. Valid (optional) arguments are: #{known_options.join(', ')}" unless invalid_options.empty?
|
105
|
+
end
|
106
|
+
|
107
|
+
# Creates the method that sets the value of an attribute. This setter
|
108
|
+
# is called both by the constructor. The constructor handles type
|
109
|
+
# casting. Setting values via the attributes hash avoids the method
|
110
|
+
# defined here.
|
111
|
+
def create_setter_for(name, klass, default)
|
112
|
+
|
113
|
+
if klass == nil
|
114
|
+
define_method "#{name}=" do |value|
|
115
|
+
attributes[name] = value
|
116
|
+
end
|
117
|
+
|
118
|
+
elsif klass == Integer
|
119
|
+
|
120
|
+
define_method "#{name}=" do |value|
|
121
|
+
value_as_integer = value && value.to_i
|
122
|
+
attributes[name] = value_as_integer
|
123
|
+
end
|
124
|
+
|
125
|
+
elsif klass == String
|
126
|
+
|
127
|
+
define_method "#{name}=" do |value|
|
128
|
+
value_as_string = value && value.to_s
|
129
|
+
attributes[name] = value_as_string
|
130
|
+
end
|
131
|
+
|
132
|
+
elsif klass == Boolean
|
133
|
+
|
134
|
+
define_method "#{name}=" do |value|
|
135
|
+
attributes[name] = !!value
|
136
|
+
end
|
137
|
+
|
138
|
+
else
|
139
|
+
|
140
|
+
define_method "#{name}=" do |value|
|
141
|
+
if value.nil?
|
142
|
+
attributes[name] = nil
|
143
|
+
elsif value.is_a? klass
|
144
|
+
attributes[name] = value
|
145
|
+
else
|
146
|
+
attributes[name] = klass.new(value)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
# creates a simple accessor method named after the attribute whose
|
153
|
+
# value it will provide during the life of the instance.
|
154
|
+
def create_accessor_for(name)
|
155
|
+
define_method name do
|
156
|
+
attributes[name]
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
# In addition to the normal getter and setter, boolean attributes
|
161
|
+
# get a method appended with a ?.
|
162
|
+
#
|
163
|
+
# class Planer < Valuable
|
164
|
+
# has_value :free_agent, :klass => Boolean
|
165
|
+
# end
|
166
|
+
#
|
167
|
+
# juan = Bus.new(:free_agent => true)
|
168
|
+
# >> juan.free_agent?
|
169
|
+
# => true
|
170
|
+
def create_question_for(name)
|
171
|
+
define_method "#{name}?" do
|
172
|
+
attributes[name]
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
# In some situations, the opposite of a value may be just as interesting.
|
177
|
+
#
|
178
|
+
# class Coder < Valuable
|
179
|
+
# has_value :agilist, :klass => Boolean, :negative => :waterfaller
|
180
|
+
# end
|
181
|
+
#
|
182
|
+
# monkey = Coder.new(:agilist => false)
|
183
|
+
# >> monkey.waterfaller?
|
184
|
+
# => true
|
185
|
+
def create_negative_question_for(name, negative)
|
186
|
+
define_method "#{negative}?" do
|
187
|
+
!attributes[name]
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
# this is a more intuitive way of marking an attribute as holding a
|
192
|
+
# collection.
|
193
|
+
#
|
194
|
+
# class Bus < Valuable
|
195
|
+
# has_collection :riders
|
196
|
+
# end
|
197
|
+
#
|
198
|
+
# >> bus = Bus.new
|
199
|
+
# >> bus.riders << 'jack'
|
200
|
+
# >> bus.riders
|
201
|
+
# => ['jack']
|
202
|
+
def has_collection(name)
|
203
|
+
has_value(name, :default => [] )
|
204
|
+
end
|
205
|
+
|
206
|
+
end
|
207
|
+
|
208
|
+
end
|
data/rakefile.rb
ADDED
@@ -0,0 +1,193 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
|
3
|
+
require 'rake'
|
4
|
+
require 'rake/rdoctask'
|
5
|
+
require 'rake/testtask'
|
6
|
+
require 'rake/packagetask'
|
7
|
+
require 'rake/gempackagetask'
|
8
|
+
require 'rake/contrib/rubyforgepublisher'
|
9
|
+
|
10
|
+
task :default => [:test]
|
11
|
+
|
12
|
+
PKG_NAME = 'valuable'
|
13
|
+
PKG_VERSION = '0.6'
|
14
|
+
PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}"
|
15
|
+
RUBY_FORGE_PROJECT = 'valuable'
|
16
|
+
RUBY_FORGE_USER = 'mustmodify'
|
17
|
+
|
18
|
+
desc "Run unit tests"
|
19
|
+
Rake::TestTask.new("test") { |t|
|
20
|
+
t.pattern = 'test/*_test.rb'
|
21
|
+
t.verbose = true
|
22
|
+
t.warning = true
|
23
|
+
}
|
24
|
+
|
25
|
+
desc 'clean temporary files'
|
26
|
+
task :clean do
|
27
|
+
temp_filenames = File.join('**', '*.*~')
|
28
|
+
temp_files = Dir.glob(temp_filenames)
|
29
|
+
if temp_files.empty?
|
30
|
+
puts 'nothing to delete'
|
31
|
+
else
|
32
|
+
puts "deleting #{temp_files.size} files"
|
33
|
+
end
|
34
|
+
|
35
|
+
File.delete(*temp_files)
|
36
|
+
end
|
37
|
+
|
38
|
+
desc 'Generate documentation for the Valuable plugin'
|
39
|
+
Rake::RDocTask.new(:rdoc) do |rdoc|
|
40
|
+
rdoc.title = 'Valuable - light weight modeling'
|
41
|
+
rdoc.options << '--line-numbers'
|
42
|
+
rdoc.options << '--inline-source'
|
43
|
+
rdoc.rdoc_files.include('README.txt')
|
44
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
45
|
+
end
|
46
|
+
|
47
|
+
spec = Gem::Specification.new do |s|
|
48
|
+
s.name = PKG_NAME
|
49
|
+
s.version = PKG_VERSION
|
50
|
+
s.platform = Gem::Platform::RUBY
|
51
|
+
s.summary = 'attr_accessor on steroids with defaults, constructor, and light casting.'
|
52
|
+
s.description = "Valuable is a ruby base class that is essentially attr_accessor on steroids. Its aim is to provide Rails-like goodness where ActiveRecord isn't an option. It intends to use a simple and intuitive interface, allowing you to get on with the logic specific to your application."
|
53
|
+
|
54
|
+
s.files = FileList["{lib, test}/**/*"].to_a + %w( README.txt rakefile.rb )
|
55
|
+
s.add_dependency 'activesupport'
|
56
|
+
s.require_path = 'lib'
|
57
|
+
s.test_files = FileList["{test}/**/*test.rb"].to_a
|
58
|
+
s.has_rdoc = false
|
59
|
+
|
60
|
+
s.rubyforge_project = RUBY_FORGE_PROJECT
|
61
|
+
s.author = 'Johnathon Wright'
|
62
|
+
s.email = 'jw@mustmodify.com'
|
63
|
+
s.homepage = 'http://valuable.mustmodify.com'
|
64
|
+
end
|
65
|
+
|
66
|
+
Rake::GemPackageTask.new(spec) do |pkg|
|
67
|
+
pkg.need_zip = true
|
68
|
+
pkg.need_tar = true
|
69
|
+
end
|
70
|
+
|
71
|
+
desc "Publish the API documentation"
|
72
|
+
task :pdoc => [:rdoc] do
|
73
|
+
Rake::RubyForgePublisher.new(RUBY_FORGE_PROJECT, RUBY_FORGE_USER).upload
|
74
|
+
end
|
75
|
+
|
76
|
+
desc 'Publish the gem and API docs'
|
77
|
+
task :publish => [:pdoc, :rubyforge_upload]
|
78
|
+
|
79
|
+
desc "Publish the release files to RubyForge."
|
80
|
+
task :rubyforge_upload => :package do
|
81
|
+
files = %w(gem tgz).map { |ext| "pkg/#{PKG_FILE_NAME}.#{ext}" }
|
82
|
+
|
83
|
+
if RUBY_FORGE_PROJECT then
|
84
|
+
require 'net/http'
|
85
|
+
require 'open-uri'
|
86
|
+
|
87
|
+
project_uri = "http://rubyforge.org/projects/#{RUBY_FORGE_PROJECT}/"
|
88
|
+
project_data = open(project_uri) { |data| data.read }
|
89
|
+
group_id = project_data[/[?&]group_id=(\d+)/, 1]
|
90
|
+
raise "Couldn't get group id" unless group_id
|
91
|
+
|
92
|
+
# This echos password to shell which is a bit sucky
|
93
|
+
if ENV["RUBY_FORGE_PASSWORD"]
|
94
|
+
password = ENV["RUBY_FORGE_PASSWORD"]
|
95
|
+
else
|
96
|
+
print "#{RUBY_FORGE_USER}@rubyforge.org's password: "
|
97
|
+
password = STDIN.gets.chomp
|
98
|
+
end
|
99
|
+
|
100
|
+
login_response = Net::HTTP.start("rubyforge.org", 80) do |http|
|
101
|
+
data = [
|
102
|
+
"login=1",
|
103
|
+
"form_loginname=#{RUBY_FORGE_USER}",
|
104
|
+
"form_pw=#{password}"
|
105
|
+
].join("&")
|
106
|
+
http.post("/account/login.php", data)
|
107
|
+
end
|
108
|
+
|
109
|
+
cookie = login_response["set-cookie"]
|
110
|
+
raise "Login failed" unless cookie
|
111
|
+
headers = { "Cookie" => cookie }
|
112
|
+
|
113
|
+
release_uri = "http://rubyforge.org/frs/admin/?group_id=#{group_id}"
|
114
|
+
release_data = open(release_uri, headers) { |data| data.read }
|
115
|
+
package_id = release_data[/[?&]package_id=(\d+)/, 1]
|
116
|
+
raise "Couldn't get package id" unless package_id
|
117
|
+
|
118
|
+
first_file = true
|
119
|
+
release_id = ""
|
120
|
+
|
121
|
+
files.each do |filename|
|
122
|
+
basename = File.basename(filename)
|
123
|
+
file_ext = File.extname(filename)
|
124
|
+
file_data = File.open(filename, "rb") { |file| file.read }
|
125
|
+
|
126
|
+
puts "Releasing #{basename}..."
|
127
|
+
|
128
|
+
release_response = Net::HTTP.start("rubyforge.org", 80) do |http|
|
129
|
+
release_date = Time.now.strftime("%Y-%m-%d %H:%M")
|
130
|
+
type_map = {
|
131
|
+
".zip" => "3000",
|
132
|
+
".tgz" => "3110",
|
133
|
+
".gz" => "3110",
|
134
|
+
".gem" => "1400"
|
135
|
+
}; type_map.default = "9999"
|
136
|
+
type = type_map[file_ext]
|
137
|
+
boundary = "rubyqMY6QN9bp6e4kS21H4y0zxcvoor"
|
138
|
+
|
139
|
+
query_hash = if first_file then
|
140
|
+
{
|
141
|
+
"group_id" => group_id,
|
142
|
+
"package_id" => package_id,
|
143
|
+
"release_name" => PKG_FILE_NAME,
|
144
|
+
"release_date" => release_date,
|
145
|
+
"type_id" => type,
|
146
|
+
"processor_id" => "8000", # Any
|
147
|
+
"release_notes" => "",
|
148
|
+
"release_changes" => "",
|
149
|
+
"preformatted" => "1",
|
150
|
+
"submit" => "1"
|
151
|
+
}
|
152
|
+
else
|
153
|
+
{
|
154
|
+
"group_id" => group_id,
|
155
|
+
"release_id" => release_id,
|
156
|
+
"package_id" => package_id,
|
157
|
+
"step2" => "1",
|
158
|
+
"type_id" => type,
|
159
|
+
"processor_id" => "8000", # Any
|
160
|
+
"submit" => "Add This File"
|
161
|
+
}
|
162
|
+
end
|
163
|
+
|
164
|
+
query = "?" + query_hash.map do |(name, value)|
|
165
|
+
[name, URI.encode(value)].join("=")
|
166
|
+
end.join("&")
|
167
|
+
|
168
|
+
data = [
|
169
|
+
"--" + boundary,
|
170
|
+
"Content-Disposition: form-data; name=\"userfile\"; filename=\"#{basename}\"",
|
171
|
+
"Content-Type: application/octet-stream",
|
172
|
+
"Content-Transfer-Encoding: binary",
|
173
|
+
"", file_data, ""
|
174
|
+
].join("\x0D\x0A")
|
175
|
+
|
176
|
+
release_headers = headers.merge(
|
177
|
+
"Content-Type" => "multipart/form-data; boundary=#{boundary}"
|
178
|
+
)
|
179
|
+
|
180
|
+
target = first_file ? "/frs/admin/qrs.php" : "/frs/admin/editrelease.php"
|
181
|
+
http.post(target + query, data, release_headers)
|
182
|
+
end
|
183
|
+
|
184
|
+
if first_file then
|
185
|
+
release_id = release_response.body[/release_id=(\d+)/, 1]
|
186
|
+
raise("Couldn't get release id") unless release_id
|
187
|
+
end
|
188
|
+
|
189
|
+
first_file = false
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
@@ -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,182 @@
|
|
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 Cubical < String
|
11
|
+
def initialize(number)
|
12
|
+
super "Lives in Cubical #{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 Developer < Valuable
|
23
|
+
has_value :experience, :klass => Integer
|
24
|
+
has_value :has_exposure_to_sunlight, :default => false
|
25
|
+
has_value :mindset
|
26
|
+
has_value :name, :default => 'DHH Jr.', :klass => String
|
27
|
+
has_value :signature, :klass => Signature
|
28
|
+
has_value :snacks_per_day, :klass => Integer, :default => 7
|
29
|
+
has_value :cubical, :klass => Cubical
|
30
|
+
has_value :hacker, :default => true
|
31
|
+
has_value :certifications, :default => DevCertifications.new
|
32
|
+
has_value :quote
|
33
|
+
has_value :employed, :klass => Boolean, :negative => 'unemployed'
|
34
|
+
|
35
|
+
has_collection :favorite_gems
|
36
|
+
|
37
|
+
end
|
38
|
+
|
39
|
+
class BaseTest < Test::Unit::TestCase
|
40
|
+
|
41
|
+
def test_that_an_attributes_hash_is_available
|
42
|
+
assert_kind_of(Hash, Developer.new.attributes)
|
43
|
+
end
|
44
|
+
|
45
|
+
def test_that_static_defaults_hash_is_available
|
46
|
+
assert_equal 'DHH Jr.', Developer.defaults[:name]
|
47
|
+
end
|
48
|
+
|
49
|
+
def test_that_an_accessor_is_created
|
50
|
+
dev = Developer.new(:mindset => :agile)
|
51
|
+
assert_equal :agile, dev.mindset
|
52
|
+
end
|
53
|
+
|
54
|
+
def test_that_setter_is_created
|
55
|
+
dev = Developer.new
|
56
|
+
dev.mindset = :enterprisey
|
57
|
+
assert_equal :enterprisey, dev.mindset
|
58
|
+
end
|
59
|
+
|
60
|
+
def test_that_attributes_can_be_cast_as_integer
|
61
|
+
dev = Developer.new(:experience => 9.2)
|
62
|
+
assert_equal 9, dev.experience
|
63
|
+
end
|
64
|
+
|
65
|
+
def test_that_integer_attributes_respect_default
|
66
|
+
assert_equal 7, Developer.new.snacks_per_day
|
67
|
+
end
|
68
|
+
|
69
|
+
def test_that_an_integer_attribute_with_no_value_results_in_nil
|
70
|
+
assert_equal nil, Developer.new.experience
|
71
|
+
end
|
72
|
+
|
73
|
+
def test_that_attributes_can_be_klassified
|
74
|
+
dev = Developer.new(:signature => 'brah brah')
|
75
|
+
assert_equal Signature, dev.signature.class
|
76
|
+
end
|
77
|
+
|
78
|
+
def test_that_defaults_appear_in_attributes_hash
|
79
|
+
assert_equal false, Developer.new.attributes[:has_exposure_to_sunlight]
|
80
|
+
end
|
81
|
+
|
82
|
+
def test_that_attributes_can_have_default_values
|
83
|
+
assert_equal false, Developer.new.has_exposure_to_sunlight
|
84
|
+
end
|
85
|
+
|
86
|
+
def test_that_randomly_classed_attributes_persist_nils
|
87
|
+
assert_equal nil, Developer.new.signature
|
88
|
+
end
|
89
|
+
|
90
|
+
def test_that_randomly_classed_attributes_respect_defaults
|
91
|
+
assert_equal 'DHH Jr.', Developer.new.name
|
92
|
+
end
|
93
|
+
|
94
|
+
def test_that_constructor_casts_attributes
|
95
|
+
assert_equal 'Lives in Cubical 20', Developer.new(:cubical => 20).cubical
|
96
|
+
end
|
97
|
+
|
98
|
+
def test_that_setter_casts_attributes
|
99
|
+
golden_boy = Developer.new
|
100
|
+
golden_boy.cubical = 20
|
101
|
+
|
102
|
+
assert_equal 'Lives in Cubical 20', golden_boy.cubical
|
103
|
+
end
|
104
|
+
|
105
|
+
def test_that_attributes_are_available_as_class_method
|
106
|
+
assert Developer.attributes.include?(:signature)
|
107
|
+
end
|
108
|
+
|
109
|
+
def test_that_a_model_can_have_a_collection
|
110
|
+
assert_equal [], Developer.new.favorite_gems
|
111
|
+
end
|
112
|
+
|
113
|
+
def test_that_values_do_not_mysteriously_jump_instances
|
114
|
+
panda = Developer.new
|
115
|
+
panda.mindset = 'geek'
|
116
|
+
|
117
|
+
hammer = Developer.new
|
118
|
+
|
119
|
+
assert_not_equal 'geek', hammer.mindset
|
120
|
+
end
|
121
|
+
|
122
|
+
def test_that_collection_values_do_not_roll_across_instances
|
123
|
+
jim = Developer.new
|
124
|
+
jim.favorite_gems << 'Ruby'
|
125
|
+
|
126
|
+
clark = Developer.new
|
127
|
+
|
128
|
+
assert_equal [], clark.favorite_gems
|
129
|
+
end
|
130
|
+
|
131
|
+
def test_that_attributes_are_cast
|
132
|
+
panda = Developer.new(:name => 'Code Panda', :experience => '8')
|
133
|
+
assert_kind_of Integer, panda.attributes[:experience]
|
134
|
+
end
|
135
|
+
|
136
|
+
def test_that_stringy_keys_are_tried_in_absence_of_symbolic_keys
|
137
|
+
homer = Developer.new('quote' => "D'oh!")
|
138
|
+
assert_equal "D'oh!", homer.quote
|
139
|
+
end
|
140
|
+
|
141
|
+
def test_that_default_values_from_seperate_instances_are_not_references_to_the_default_value_for_that_field
|
142
|
+
assert_not_equal Developer.new.favorite_gems.object_id, Developer.new.favorite_gems.object_id
|
143
|
+
end
|
144
|
+
|
145
|
+
def test_that_properly_klassed_values_are_not_rekast
|
146
|
+
why_hammer = Signature.new('go ask your mom')
|
147
|
+
Signature.expects(:new).with(why_hammer).never
|
148
|
+
hammer = Developer.new(:signature => why_hammer)
|
149
|
+
end
|
150
|
+
|
151
|
+
def test_that_values_can_be_set_to_false
|
152
|
+
assert_equal false, Developer.new(:hacker => false).hacker
|
153
|
+
end
|
154
|
+
|
155
|
+
def test_that_default_values_needing_deep_duplication_get_it
|
156
|
+
a = Developer.new
|
157
|
+
b = Developer.new
|
158
|
+
|
159
|
+
a.certifications.hash_rocket = true
|
160
|
+
assert_equal false, b.certifications.hash_rocket
|
161
|
+
end
|
162
|
+
|
163
|
+
def test_that_default_values_can_be_set_to_nothing
|
164
|
+
assert_equal nil, Developer.new(:hacker => nil).hacker
|
165
|
+
end
|
166
|
+
|
167
|
+
def test_that_values_are_cast_to_boolean
|
168
|
+
assert_equal false, Developer.new(:employed => nil).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
|
+
end
|
metadata
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: valuable
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: "0.6"
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Johnathon Wright
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-06-22 00:00:00 -05:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: activesupport
|
17
|
+
type: :runtime
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: "0"
|
24
|
+
version:
|
25
|
+
description: Valuable is a ruby base class that is essentially attr_accessor on steroids. Its aim is to provide Rails-like goodness where ActiveRecord isn't an option. It intends to use a simple and intuitive interface, allowing you to get on with the logic specific to your application.
|
26
|
+
email: jw@mustmodify.com
|
27
|
+
executables: []
|
28
|
+
|
29
|
+
extensions: []
|
30
|
+
|
31
|
+
extra_rdoc_files: []
|
32
|
+
|
33
|
+
files:
|
34
|
+
- lib/boolean.rb
|
35
|
+
- lib/valuable.rb
|
36
|
+
- README.txt
|
37
|
+
- rakefile.rb
|
38
|
+
has_rdoc: true
|
39
|
+
homepage: http://valuable.mustmodify.com
|
40
|
+
licenses: []
|
41
|
+
|
42
|
+
post_install_message:
|
43
|
+
rdoc_options: []
|
44
|
+
|
45
|
+
require_paths:
|
46
|
+
- lib
|
47
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
48
|
+
requirements:
|
49
|
+
- - ">="
|
50
|
+
- !ruby/object:Gem::Version
|
51
|
+
version: "0"
|
52
|
+
version:
|
53
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
54
|
+
requirements:
|
55
|
+
- - ">="
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
version: "0"
|
58
|
+
version:
|
59
|
+
requirements: []
|
60
|
+
|
61
|
+
rubyforge_project: valuable
|
62
|
+
rubygems_version: 1.3.3
|
63
|
+
signing_key:
|
64
|
+
specification_version: 3
|
65
|
+
summary: attr_accessor on steroids with defaults, constructor, and light casting.
|
66
|
+
test_files:
|
67
|
+
- test/bad_attributes_test.rb
|
68
|
+
- test/valuable_test.rb
|