valuable 0.6 → 0.8

Sign up to get free protection for your applications and to get access to all the features.
@@ -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`.
@@ -1,40 +1,48 @@
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.
1
+ # Valuable is the class from which all classes (who are so inclined)
2
+ # should inherit.
5
3
  #
6
- # Example:
4
+ # ==Example:
7
5
  #
8
- # class Bus < Valuable
9
- # has_value :color, :default => 'yellow'
10
- # has_value :number, :klass => Integer
11
- # has_collection :riders
12
- # end
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'}
13
19
  class Valuable
14
20
 
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.
21
+ # Returns a Hash representing all known values. Values are set three ways:
19
22
  #
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
+ # (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.
23
32
  #
24
- # >> bus = Bus.new(:number => 16) # color has default value 'yellow'
25
- # >> bus.attributes
26
- # => {:color => 'yellow', :number => 16}
33
+ # >> bus = Bus.new(:number => 16) # color has default value 'yellow'
34
+ # >> bus.attributes
35
+ # => {:color => 'yellow', :number => 16}
27
36
  def attributes
28
- @attributes ||= HashWithIndifferentAccess.new(deep_duplicate_of(self.class.defaults))
37
+ @attributes ||= deep_duplicate_of(self.class.defaults)
29
38
  end
30
39
 
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 ) }
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
35
44
  end
36
45
 
37
- # Creates a duplicate of all values.
38
46
  def deep_duplicate_of(value)
39
47
  Marshal.load(Marshal.dump(value))
40
48
  end
@@ -49,92 +57,88 @@ class Valuable
49
57
  # Returns a name/value set of the values that will be used on
50
58
  # instanciation unless new values are provided.
51
59
  #
52
- # class Bus < Valuable
53
- # has_value :color, :default => 'yellow'
54
- # end
55
- #
56
- # >> Bus.defaults
57
- # => {:color => 'yellow'}
58
- #
60
+ # >> Bus.defaults
61
+ # => {:color => 'yellow'}
59
62
  def defaults
60
63
  @defaults ||= {}
61
64
  end
62
65
 
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.
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.
67
73
  #
68
- # :default - for the given attribute, use this value if no other is
69
- # provided.
74
+ # :klass - light weight type casting. Use :integer, :string or
75
+ # :boolean. Alternately, supply a class.
70
76
  #
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.
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.
75
82
  #
76
- # A great example: PhoneNumber < String is useful if you
83
+ # A good example: PhoneNumber < String is useful if you
77
84
  # want numbers to come out the other end properly formatted, when your
78
85
  # input may come in as an integer, or string without formatting, or
79
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.
80
92
  def has_value(name, options={})
93
+
94
+ name = name.to_sym
95
+
81
96
  attributes << name
82
97
 
83
98
  defaults[name] = options[:default] unless options[:default].nil?
84
99
 
85
100
  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]
101
+ create_question_for(name) if options[:klass] == :boolean
102
+ create_negative_question_for(name, options[:negative]) if options[:klass] == :boolean && options[:negative]
88
103
 
89
104
  create_setter_for(name, options[:klass], options[:default])
90
105
 
91
106
  check_options_validity(name, options)
92
107
  end
93
108
 
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
109
  # Creates the method that sets the value of an attribute. This setter
108
110
  # is called both by the constructor. The constructor handles type
109
111
  # casting. Setting values via the attributes hash avoids the method
110
112
  # defined here.
111
113
  def create_setter_for(name, klass, default)
112
-
113
- if klass == nil
114
+
115
+ case klass
116
+ when NilClass
117
+
114
118
  define_method "#{name}=" do |value|
115
119
  attributes[name] = value
116
120
  end
117
121
 
118
- elsif klass == Integer
122
+ when :integer
119
123
 
120
124
  define_method "#{name}=" do |value|
121
125
  value_as_integer = value && value.to_i
122
126
  attributes[name] = value_as_integer
123
127
  end
124
128
 
125
- elsif klass == String
129
+ when :string
126
130
 
127
131
  define_method "#{name}=" do |value|
128
132
  value_as_string = value && value.to_s
129
133
  attributes[name] = value_as_string
130
134
  end
131
135
 
132
- elsif klass == Boolean
136
+ when :boolean
133
137
 
134
138
  define_method "#{name}=" do |value|
135
- attributes[name] = !!value
139
+ attributes[name] = value == '0' ? false : !!value
136
140
  end
137
-
141
+
138
142
  else
139
143
 
140
144
  define_method "#{name}=" do |value|
@@ -160,13 +164,13 @@ class Valuable
160
164
  # In addition to the normal getter and setter, boolean attributes
161
165
  # get a method appended with a ?.
162
166
  #
163
- # class Planer < Valuable
164
- # has_value :free_agent, :klass => Boolean
165
- # end
167
+ # class Player < Valuable
168
+ # has_value :free_agent, :klass => Boolean
169
+ # end
166
170
  #
167
- # juan = Bus.new(:free_agent => true)
168
- # >> juan.free_agent?
169
- # => true
171
+ # juan = Player.new(:free_agent => true)
172
+ # >> juan.free_agent?
173
+ # => true
170
174
  def create_question_for(name)
171
175
  define_method "#{name}?" do
172
176
  attributes[name]
@@ -175,13 +179,13 @@ class Valuable
175
179
 
176
180
  # In some situations, the opposite of a value may be just as interesting.
177
181
  #
178
- # class Coder < Valuable
179
- # has_value :agilist, :klass => Boolean, :negative => :waterfaller
180
- # end
182
+ # class Coder < Valuable
183
+ # has_value :agilist, :klass => Boolean, :negative => :waterfaller
184
+ # end
181
185
  #
182
- # monkey = Coder.new(:agilist => false)
183
- # >> monkey.waterfaller?
184
- # => true
186
+ # monkey = Coder.new(:agilist => false)
187
+ # >> monkey.waterfaller?
188
+ # => true
185
189
  def create_negative_question_for(name, negative)
186
190
  define_method "#{negative}?" do
187
191
  !attributes[name]
@@ -191,18 +195,39 @@ class Valuable
191
195
  # this is a more intuitive way of marking an attribute as holding a
192
196
  # collection.
193
197
  #
194
- # class Bus < Valuable
195
- # has_collection :riders
196
- # end
198
+ # class Bus < Valuable
199
+ # has_value :riders, :default => [] # meh...
200
+ # has_collection :riders # better!
201
+ # end
197
202
  #
198
- # >> bus = Bus.new
199
- # >> bus.riders << 'jack'
200
- # >> bus.riders
201
- # => ['jack']
203
+ # >> bus = Bus.new
204
+ # >> bus.riders << 'jack'
205
+ # >> bus.riders
206
+ # => ['jack']
202
207
  def has_collection(name)
203
208
  has_value(name, :default => [] )
204
209
  end
205
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
+
206
231
  end
207
232
 
208
233
  end
@@ -1,193 +1,76 @@
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
-
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
+
@@ -1,23 +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
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
@@ -4,9 +4,6 @@ require 'test/unit'
4
4
  require 'valuable.rb'
5
5
  require 'mocha'
6
6
 
7
- class Signature < String
8
- end
9
-
10
7
  class Cubical < String
11
8
  def initialize(number)
12
9
  super "Lives in Cubical #{number}"
@@ -20,17 +17,16 @@ class DevCertifications < Valuable
20
17
  end
21
18
 
22
19
  class Developer < Valuable
23
- has_value :experience, :klass => Integer
20
+ has_value :experience, :klass => :integer
24
21
  has_value :has_exposure_to_sunlight, :default => false
25
22
  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
23
+ has_value :name, :default => 'DHH Jr.', :klass => :string
24
+ has_value :snacks_per_day, :klass => :integer, :default => 7
29
25
  has_value :cubical, :klass => Cubical
30
26
  has_value :hacker, :default => true
31
- has_value :certifications, :default => DevCertifications.new
27
+ has_value :certifications, :klass => DevCertifications, :default => DevCertifications.new
32
28
  has_value :quote
33
- has_value :employed, :klass => Boolean, :negative => 'unemployed'
29
+ has_value :employed, :klass => :boolean, :negative => 'unemployed'
34
30
 
35
31
  has_collection :favorite_gems
36
32
 
@@ -71,8 +67,8 @@ class BaseTest < Test::Unit::TestCase
71
67
  end
72
68
 
73
69
  def test_that_attributes_can_be_klassified
74
- dev = Developer.new(:signature => 'brah brah')
75
- assert_equal Signature, dev.signature.class
70
+ dev = Developer.new(:cubical => 12)
71
+ assert_equal Cubical, dev.cubical.class
76
72
  end
77
73
 
78
74
  def test_that_defaults_appear_in_attributes_hash
@@ -84,7 +80,7 @@ class BaseTest < Test::Unit::TestCase
84
80
  end
85
81
 
86
82
  def test_that_randomly_classed_attributes_persist_nils
87
- assert_equal nil, Developer.new.signature
83
+ assert_equal nil, Developer.new.cubical
88
84
  end
89
85
 
90
86
  def test_that_randomly_classed_attributes_respect_defaults
@@ -103,7 +99,7 @@ class BaseTest < Test::Unit::TestCase
103
99
  end
104
100
 
105
101
  def test_that_attributes_are_available_as_class_method
106
- assert Developer.attributes.include?(:signature)
102
+ assert Developer.attributes.include?(:cubical)
107
103
  end
108
104
 
109
105
  def test_that_a_model_can_have_a_collection
@@ -143,9 +139,9 @@ class BaseTest < Test::Unit::TestCase
143
139
  end
144
140
 
145
141
  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)
142
+ stapler = Cubical.new('in sub-basement')
143
+ Cubical.expects(:new).with(stapler).never
144
+ Developer.new(:cubical => stapler)
149
145
  end
150
146
 
151
147
  def test_that_values_can_be_set_to_false
@@ -168,6 +164,10 @@ class BaseTest < Test::Unit::TestCase
168
164
  assert_equal false, Developer.new(:employed => nil).employed
169
165
  end
170
166
 
167
+ def test_that_string_zero_becomes_false
168
+ assert_equal false, Developer.new(:employed => '0').employed
169
+ end
170
+
171
171
  def test_that_boolean_values_get_questionmarked_methods
172
172
  assert Developer.instance_methods.include?('employed?')
173
173
  end
@@ -179,4 +179,16 @@ class BaseTest < Test::Unit::TestCase
179
179
  def test_that_negative_methods_are_negative
180
180
  assert_equal true, Developer.new(:employed => false).unemployed?
181
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
182
194
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: valuable
3
3
  version: !ruby/object:Gem::Version
4
- version: "0.6"
4
+ version: "0.8"
5
5
  platform: ruby
6
6
  authors:
7
7
  - Johnathon Wright
@@ -9,20 +9,11 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-06-22 00:00:00 -05:00
12
+ date: 2009-12-16 00:00:00 -06:00
13
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.
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.
26
17
  email: jw@mustmodify.com
27
18
  executables: []
28
19
 
@@ -31,12 +22,11 @@ extensions: []
31
22
  extra_rdoc_files: []
32
23
 
33
24
  files:
34
- - lib/boolean.rb
35
25
  - lib/valuable.rb
36
- - README.txt
26
+ - README.markdown
37
27
  - rakefile.rb
38
28
  has_rdoc: true
39
- homepage: http://valuable.mustmodify.com
29
+ homepage: http://www.github.com/mustmodify/valuable
40
30
  licenses: []
41
31
 
42
32
  post_install_message:
@@ -59,10 +49,12 @@ required_rubygems_version: !ruby/object:Gem::Requirement
59
49
  requirements: []
60
50
 
61
51
  rubyforge_project: valuable
62
- rubygems_version: 1.3.3
52
+ rubygems_version: 1.3.5
63
53
  signing_key:
64
54
  specification_version: 3
65
55
  summary: attr_accessor on steroids with defaults, constructor, and light casting.
66
56
  test_files:
67
57
  - test/bad_attributes_test.rb
58
+ - test/deprecated_test.rb
59
+ - test/inheritance_test.rb
68
60
  - test/valuable_test.rb
data/README.txt DELETED
@@ -1,79 +0,0 @@
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.
@@ -1,2 +0,0 @@
1
- module Boolean
2
- end