datamapper 0.1.0
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/CHANGELOG +2 -0
- data/MIT-LICENSE +22 -0
- data/README +1 -0
- data/example.rb +25 -0
- data/lib/data_mapper.rb +30 -0
- data/lib/data_mapper/adapters/abstract_adapter.rb +229 -0
- data/lib/data_mapper/adapters/mysql_adapter.rb +171 -0
- data/lib/data_mapper/adapters/sqlite3_adapter.rb +189 -0
- data/lib/data_mapper/associations.rb +19 -0
- data/lib/data_mapper/associations/belongs_to_association.rb +111 -0
- data/lib/data_mapper/associations/has_and_belongs_to_many_association.rb +100 -0
- data/lib/data_mapper/associations/has_many_association.rb +101 -0
- data/lib/data_mapper/associations/has_one_association.rb +107 -0
- data/lib/data_mapper/base.rb +160 -0
- data/lib/data_mapper/callbacks.rb +47 -0
- data/lib/data_mapper/database.rb +134 -0
- data/lib/data_mapper/extensions/active_record_impersonation.rb +69 -0
- data/lib/data_mapper/extensions/callback_helpers.rb +35 -0
- data/lib/data_mapper/identity_map.rb +21 -0
- data/lib/data_mapper/loaded_set.rb +45 -0
- data/lib/data_mapper/mappings/column.rb +78 -0
- data/lib/data_mapper/mappings/schema.rb +28 -0
- data/lib/data_mapper/mappings/table.rb +99 -0
- data/lib/data_mapper/queries/conditions.rb +141 -0
- data/lib/data_mapper/queries/connection.rb +34 -0
- data/lib/data_mapper/queries/create_table_statement.rb +38 -0
- data/lib/data_mapper/queries/delete_statement.rb +17 -0
- data/lib/data_mapper/queries/drop_table_statement.rb +17 -0
- data/lib/data_mapper/queries/insert_statement.rb +29 -0
- data/lib/data_mapper/queries/reader.rb +42 -0
- data/lib/data_mapper/queries/result.rb +19 -0
- data/lib/data_mapper/queries/select_statement.rb +103 -0
- data/lib/data_mapper/queries/table_exists_statement.rb +17 -0
- data/lib/data_mapper/queries/truncate_table_statement.rb +17 -0
- data/lib/data_mapper/queries/update_statement.rb +25 -0
- data/lib/data_mapper/session.rb +240 -0
- data/lib/data_mapper/support/blank_slate.rb +3 -0
- data/lib/data_mapper/support/connection_pool.rb +117 -0
- data/lib/data_mapper/support/enumerable.rb +27 -0
- data/lib/data_mapper/support/inflector.rb +329 -0
- data/lib/data_mapper/support/proc.rb +69 -0
- data/lib/data_mapper/support/string.rb +23 -0
- data/lib/data_mapper/support/symbol.rb +91 -0
- data/lib/data_mapper/support/weak_hash.rb +46 -0
- data/lib/data_mapper/unit_of_work.rb +38 -0
- data/lib/data_mapper/validations/confirmation_validator.rb +55 -0
- data/lib/data_mapper/validations/contextual_validations.rb +50 -0
- data/lib/data_mapper/validations/format_validator.rb +85 -0
- data/lib/data_mapper/validations/formats/email.rb +78 -0
- data/lib/data_mapper/validations/generic_validator.rb +27 -0
- data/lib/data_mapper/validations/length_validator.rb +75 -0
- data/lib/data_mapper/validations/required_field_validator.rb +47 -0
- data/lib/data_mapper/validations/unique_validator.rb +65 -0
- data/lib/data_mapper/validations/validation_errors.rb +34 -0
- data/lib/data_mapper/validations/validation_helper.rb +60 -0
- data/performance.rb +156 -0
- data/profile_data_mapper.rb +18 -0
- data/rakefile.rb +80 -0
- data/spec/basic_finder.rb +67 -0
- data/spec/belongs_to.rb +47 -0
- data/spec/fixtures/animals.yaml +32 -0
- data/spec/fixtures/exhibits.yaml +90 -0
- data/spec/fixtures/fruit.yaml +6 -0
- data/spec/fixtures/people.yaml +15 -0
- data/spec/fixtures/zoos.yaml +20 -0
- data/spec/has_and_belongs_to_many.rb +25 -0
- data/spec/has_many.rb +34 -0
- data/spec/legacy.rb +14 -0
- data/spec/models/animal.rb +7 -0
- data/spec/models/exhibit.rb +6 -0
- data/spec/models/fruit.rb +6 -0
- data/spec/models/person.rb +7 -0
- data/spec/models/post.rb +4 -0
- data/spec/models/sales_person.rb +4 -0
- data/spec/models/zoo.rb +5 -0
- data/spec/new_record.rb +24 -0
- data/spec/spec_helper.rb +61 -0
- data/spec/sub_select.rb +16 -0
- data/spec/symbolic_operators.rb +21 -0
- data/spec/validates_confirmation_of.rb +36 -0
- data/spec/validates_format_of.rb +61 -0
- data/spec/validates_length_of.rb +101 -0
- data/spec/validates_uniqueness_of.rb +45 -0
- data/spec/validations.rb +63 -0
- metadata +134 -0
data/spec/legacy.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
context 'Legacy Database' do
|
2
|
+
|
3
|
+
specify('should allow models to map with custom attribute names') do
|
4
|
+
Fruit.first.name.should == 'Kiwi'
|
5
|
+
end
|
6
|
+
|
7
|
+
specify('should allow custom foreign-key mappings') do
|
8
|
+
database do
|
9
|
+
Fruit[:name => 'Watermelon'].devourer_of_souls.should == Animal[:name => 'Cup']
|
10
|
+
Animal[:name => 'Cup'].favourite_fruit.should == Fruit[:name => 'Watermelon']
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
data/spec/models/post.rb
ADDED
data/spec/models/zoo.rb
ADDED
data/spec/new_record.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
context 'A new record' do
|
2
|
+
|
3
|
+
setup do
|
4
|
+
@bob = Person.new(:name => 'Bob', :age => 30, :occupation => 'Sales')
|
5
|
+
end
|
6
|
+
|
7
|
+
specify 'should be dirty' do
|
8
|
+
@bob.dirty?.should == true
|
9
|
+
end
|
10
|
+
|
11
|
+
specify 'set attributes should be dirty' do
|
12
|
+
attributes = @bob.attributes.dup.reject { |k,v| k == :id }
|
13
|
+
@bob.dirty_attributes.should == { :name => 'Bob', :age => 30, :occupation => 'Sales' }
|
14
|
+
end
|
15
|
+
|
16
|
+
specify 'should be marked as new' do
|
17
|
+
@bob.new_record?.should == true
|
18
|
+
end
|
19
|
+
|
20
|
+
specify 'should have a nil id' do
|
21
|
+
@bob.id.should == nil
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../lib/data_mapper'
|
2
|
+
require 'yaml'
|
3
|
+
require 'pp'
|
4
|
+
|
5
|
+
log_path = File.dirname(__FILE__) + '/../spec.log'
|
6
|
+
|
7
|
+
require 'fileutils'
|
8
|
+
FileUtils::rm log_path if File.exists?(log_path)
|
9
|
+
|
10
|
+
case ENV['ADAPTER']
|
11
|
+
when 'sqlite3' then
|
12
|
+
DataMapper::Database.setup do
|
13
|
+
adapter 'sqlite3'
|
14
|
+
database 'data_mapper_1.db'
|
15
|
+
log_stream 'spec.log'
|
16
|
+
log_level Logger::DEBUG
|
17
|
+
end
|
18
|
+
else
|
19
|
+
DataMapper::Database.setup do
|
20
|
+
adapter 'mysql'
|
21
|
+
database 'data_mapper_1'
|
22
|
+
username 'root'
|
23
|
+
log_stream 'spec.log'
|
24
|
+
log_level Logger::DEBUG
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
Dir[File.dirname(__FILE__) + '/models/*.rb'].each do |path|
|
29
|
+
load path
|
30
|
+
end
|
31
|
+
|
32
|
+
database do |db|
|
33
|
+
db.schema.each do |table|
|
34
|
+
db.create_table(table.klass)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
at_exit do
|
39
|
+
database do |db|
|
40
|
+
db.schema.each do |table|
|
41
|
+
db.drop_table(table.klass)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end if ENV['DROP'] == '1'
|
45
|
+
|
46
|
+
# Define a fixtures helper method to load up our test data.
|
47
|
+
def fixtures(name)
|
48
|
+
entry = YAML::load_file(File.dirname(__FILE__) + "/fixtures/#{name}.yaml")
|
49
|
+
klass = Kernel::const_get(Inflector.classify(Inflector.singularize(name)))
|
50
|
+
|
51
|
+
klass.truncate!
|
52
|
+
|
53
|
+
(entry.kind_of?(Array) ? entry : [entry]).each do |hash|
|
54
|
+
klass::create(hash)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# Pre-fill the database so non-destructive tests don't need to reload fixtures.
|
59
|
+
Dir[File.dirname(__FILE__) + "/fixtures/*.yaml"].each do |path|
|
60
|
+
fixtures(File::basename(path).sub(/\.yaml$/, ''))
|
61
|
+
end
|
data/spec/sub_select.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
=begin
|
2
|
+
context 'Sub-selection' do
|
3
|
+
|
4
|
+
specify 'should return a Cup' do
|
5
|
+
Animal[:id.select => { :name => 'cup' }].name.should == 'Cup'
|
6
|
+
end
|
7
|
+
|
8
|
+
specify 'should return all exhibits for Galveston zoo' do
|
9
|
+
Exhibit.all(:zoo_id.select(Zoo) => { :name => 'Galveston' }).size.should == 3
|
10
|
+
end
|
11
|
+
|
12
|
+
specify 'should allow a sub-select in the select-list' do
|
13
|
+
Animal[:select => [ :id.count ]]
|
14
|
+
end
|
15
|
+
end
|
16
|
+
=end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
context 'Symbolic Operators' do
|
2
|
+
|
3
|
+
specify('should use greater_than_or_equal_to to limit results') do
|
4
|
+
Person.all(:age.gte => 28).size.should == 3
|
5
|
+
end
|
6
|
+
|
7
|
+
specify('use an Array for in-clauses') do
|
8
|
+
family = Person.all(:id => [1, 2, 4])
|
9
|
+
family[0].name.should == 'Sam'
|
10
|
+
family[1].name.should == 'Amy'
|
11
|
+
family[2].name.should == 'Josh'
|
12
|
+
end
|
13
|
+
|
14
|
+
specify('use "not" for not-equal operations') do
|
15
|
+
Person.all(:name.not => 'Bob').size.should == 4
|
16
|
+
end
|
17
|
+
|
18
|
+
specify('age should not be nil') do
|
19
|
+
Person.all(:age.not => nil).size.should == 5
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
context 'A Cow' do
|
2
|
+
|
3
|
+
setup do
|
4
|
+
class Cow
|
5
|
+
|
6
|
+
include DataMapper::Extensions::ValidationHelper
|
7
|
+
|
8
|
+
attr_accessor :name, :name_confirmation, :age
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
specify('') do
|
13
|
+
class Cow
|
14
|
+
validations.clear!
|
15
|
+
validates_confirmation_of :name, :context => :save
|
16
|
+
end
|
17
|
+
|
18
|
+
betsy = Cow.new
|
19
|
+
betsy.valid?.should == true
|
20
|
+
|
21
|
+
betsy.name = 'Betsy'
|
22
|
+
betsy.name_confirmation = ''
|
23
|
+
betsy.valid?(:save).should == false
|
24
|
+
betsy.errors.full_messages.first.should == 'Name does not match the confirmation'
|
25
|
+
|
26
|
+
betsy.name = ''
|
27
|
+
betsy.name_confirmation = 'Betsy'
|
28
|
+
betsy.valid?(:save).should == false
|
29
|
+
betsy.errors.full_messages.first.should == 'Name does not match the confirmation'
|
30
|
+
|
31
|
+
betsy.name = 'Betsy'
|
32
|
+
betsy.name_confirmation = 'Betsy'
|
33
|
+
betsy.valid?(:save).should == true
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
context 'An Employee' do
|
2
|
+
|
3
|
+
setup do
|
4
|
+
class Employee
|
5
|
+
|
6
|
+
include DataMapper::Extensions::ValidationHelper
|
7
|
+
|
8
|
+
attr_accessor :email
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
specify('must have a valid email address') do
|
13
|
+
class Employee
|
14
|
+
validations.clear!
|
15
|
+
validates_format_of :email, :as => :email_address, :on => :save
|
16
|
+
end
|
17
|
+
|
18
|
+
e = Employee.new
|
19
|
+
e.valid?.should == true
|
20
|
+
|
21
|
+
[
|
22
|
+
'test test@example.com', 'test@example', 'test#example.com',
|
23
|
+
'tester@exampl$.com', '[scizzle]@example.com', '.test@example.com'
|
24
|
+
].all? { |test_email|
|
25
|
+
e.email = test_email
|
26
|
+
e.valid?(:save).should == false
|
27
|
+
e.errors.full_messages.first.should == "#{test_email} is not a valid email address"
|
28
|
+
}
|
29
|
+
|
30
|
+
e.email = 'test@example.com'
|
31
|
+
e.valid?(:save).should == true
|
32
|
+
end
|
33
|
+
|
34
|
+
specify('must have a valid organization code') do
|
35
|
+
class Employee
|
36
|
+
validations.clear!
|
37
|
+
|
38
|
+
attr_accessor :organization_code
|
39
|
+
|
40
|
+
# WARNING: contrived example
|
41
|
+
# The organization code must be A#### or B######X12
|
42
|
+
validates_format_of :organization_code, :on => :save, :with => lambda { |code|
|
43
|
+
(code =~ /A\d{4}/) || (code =~ /[B-Z]\d{6}X12/)
|
44
|
+
}
|
45
|
+
end
|
46
|
+
|
47
|
+
e = Employee.new
|
48
|
+
e.valid?.should == true
|
49
|
+
|
50
|
+
e.organization_code = 'BLAH :)'
|
51
|
+
e.valid?(:save).should == false
|
52
|
+
e.errors.full_messages.first.should == 'Organization code is invalid'
|
53
|
+
|
54
|
+
e.organization_code = 'A1234'
|
55
|
+
e.valid?(:save).should == true
|
56
|
+
|
57
|
+
e.organization_code = 'B123456X12'
|
58
|
+
e.valid?(:save).should == true
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
context 'A Cow' do
|
2
|
+
|
3
|
+
setup do
|
4
|
+
class Cow
|
5
|
+
|
6
|
+
include DataMapper::Extensions::ValidationHelper
|
7
|
+
|
8
|
+
attr_accessor :name, :age
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
specify('should not have a name shorter than 3 characters') do
|
13
|
+
class Cow
|
14
|
+
validations.clear!
|
15
|
+
validates_length_of :name, :min => 3, :context => :save
|
16
|
+
end
|
17
|
+
|
18
|
+
betsy = Cow.new
|
19
|
+
betsy.valid?.should == true
|
20
|
+
|
21
|
+
betsy.valid?(:save).should == false
|
22
|
+
betsy.errors.full_messages.first.should == 'Name must be more than 3 characters long'
|
23
|
+
|
24
|
+
betsy.name = 'Be'
|
25
|
+
betsy.valid?(:save).should == false
|
26
|
+
betsy.errors.full_messages.first.should == 'Name must be more than 3 characters long'
|
27
|
+
|
28
|
+
betsy.name = 'Bet'
|
29
|
+
betsy.valid?(:save).should == true
|
30
|
+
|
31
|
+
betsy.name = 'Bets'
|
32
|
+
betsy.valid?(:save).should == true
|
33
|
+
end
|
34
|
+
|
35
|
+
|
36
|
+
specify('should not have a name longer than 10 characters') do
|
37
|
+
class Cow
|
38
|
+
validations.clear!
|
39
|
+
validates_length_of :name, :max => 10, :context => :save
|
40
|
+
end
|
41
|
+
|
42
|
+
betsy = Cow.new
|
43
|
+
betsy.valid?.should == true
|
44
|
+
betsy.valid?(:save).should == true
|
45
|
+
|
46
|
+
betsy.name = 'Testicular Fortitude'
|
47
|
+
betsy.valid?(:save).should == false
|
48
|
+
betsy.errors.full_messages.first.should == 'Name must be less than 10 characters long'
|
49
|
+
|
50
|
+
betsy.name = 'Betsy'
|
51
|
+
betsy.valid?(:save).should == true
|
52
|
+
end
|
53
|
+
|
54
|
+
specify('should have a name that is 8 characters long') do
|
55
|
+
class Cow
|
56
|
+
validations.clear!
|
57
|
+
validates_length_of :name, :is => 8, :context => :save
|
58
|
+
end
|
59
|
+
|
60
|
+
# Context is not save
|
61
|
+
betsy = Cow.new
|
62
|
+
betsy.valid?.should == true
|
63
|
+
|
64
|
+
# Context is :save
|
65
|
+
betsy.valid?(:save).should == false
|
66
|
+
|
67
|
+
betsy.name = 'Testicular Fortitude'
|
68
|
+
betsy.valid?(:save).should == false
|
69
|
+
betsy.errors.full_messages.first.should == 'Name must be 8 characters long'
|
70
|
+
|
71
|
+
betsy.name = 'Samooela'
|
72
|
+
betsy.valid?(:save).should == true
|
73
|
+
end
|
74
|
+
|
75
|
+
specify('should have a name that is between 10 and 15 characters long') do
|
76
|
+
class Cow
|
77
|
+
validations.clear!
|
78
|
+
validates_length_of :name, :in => (10..15), :context => :save
|
79
|
+
end
|
80
|
+
|
81
|
+
# Context is not save
|
82
|
+
betsy = Cow.new
|
83
|
+
betsy.valid?.should == true
|
84
|
+
|
85
|
+
# Context is :save
|
86
|
+
betsy.valid?(:save)
|
87
|
+
betsy.errors.full_messages.first
|
88
|
+
|
89
|
+
betsy.valid?(:save).should == false
|
90
|
+
betsy.errors.full_messages.first.should == 'Name must be between 10 and 15 characters long'
|
91
|
+
|
92
|
+
betsy.name = 'Smoooooot'
|
93
|
+
betsy.valid?(:save).should == false
|
94
|
+
|
95
|
+
betsy.name = 'Smooooooooooooooooooot'
|
96
|
+
betsy.valid?(:save).should == false
|
97
|
+
|
98
|
+
betsy.name = 'Smootenstein'
|
99
|
+
betsy.valid?(:save).should == true
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
context 'An Animal' do
|
2
|
+
|
3
|
+
specify('must have a unique name') do
|
4
|
+
class Animal
|
5
|
+
validations.clear!
|
6
|
+
validates_uniqueness_of :name, :context => :save
|
7
|
+
end
|
8
|
+
|
9
|
+
bugaboo = Animal.new
|
10
|
+
bugaboo.valid?.should == true
|
11
|
+
|
12
|
+
bugaboo.name = 'Bear'
|
13
|
+
bugaboo.valid?(:save).should == false
|
14
|
+
bugaboo.errors.full_messages.first.should == 'Name has already been taken'
|
15
|
+
|
16
|
+
bugaboo.name = 'Bugaboo'
|
17
|
+
bugaboo.valid?(:save).should == true
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
|
22
|
+
context 'A Person' do
|
23
|
+
setup do
|
24
|
+
fixtures 'people'
|
25
|
+
end
|
26
|
+
|
27
|
+
specify('must have a unique name for their occupation') do
|
28
|
+
class Person
|
29
|
+
validations.clear!
|
30
|
+
validates_uniqueness_of :name, :context => :save, :scope => :occupation
|
31
|
+
end
|
32
|
+
|
33
|
+
new_programmer_scott = Person.new(:name => 'Scott', :age => 29, :occupation => 'Programmer')
|
34
|
+
garbage_man_scott = Person.new(:name => 'Scott', :age => 25, :occupation => 'Garbage Man')
|
35
|
+
|
36
|
+
# Should be valid even though there is another 'Scott' already in the database
|
37
|
+
garbage_man_scott.valid?(:save).should == true
|
38
|
+
|
39
|
+
# Should NOT be valid, there is already a Programmer names Scott, adding one more
|
40
|
+
# would destroy the universe or something
|
41
|
+
new_programmer_scott.valid?(:save).should == false
|
42
|
+
new_programmer_scott.errors.full_messages.first.should == "Name has already been taken"
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
data/spec/validations.rb
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
context 'Validations' do
|
2
|
+
|
3
|
+
setup do
|
4
|
+
class Cow
|
5
|
+
|
6
|
+
include DataMapper::Extensions::ValidationHelper
|
7
|
+
|
8
|
+
attr_accessor :name, :age
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
specify('should allow you to specify not-null fields in different contexts') do
|
13
|
+
class Cow
|
14
|
+
validations.clear!
|
15
|
+
validates_presence_of :name, :context => :save
|
16
|
+
end
|
17
|
+
|
18
|
+
betsy = Cow.new
|
19
|
+
betsy.valid?.should == true
|
20
|
+
|
21
|
+
betsy.valid?(:save).should == false
|
22
|
+
betsy.errors.full_messages.first.should == 'Name must not be blank'
|
23
|
+
|
24
|
+
betsy.name = 'Betsy'
|
25
|
+
betsy.valid?(:save).should == true
|
26
|
+
end
|
27
|
+
|
28
|
+
specify('should be able to use ":on" for a context alias') do
|
29
|
+
class Cow
|
30
|
+
validations.clear!
|
31
|
+
validates_presence_of :name, :age, :on => :create
|
32
|
+
end
|
33
|
+
|
34
|
+
maggie = Cow.new
|
35
|
+
maggie.valid?.should == true
|
36
|
+
|
37
|
+
maggie.valid?(:create).should == false
|
38
|
+
maggie.errors.full_messages.should include('Age must not be blank')
|
39
|
+
maggie.errors.full_messages.should include('Name must not be blank')
|
40
|
+
|
41
|
+
maggie.name = 'Maggie'
|
42
|
+
maggie.age = 29
|
43
|
+
maggie.valid?(:create).should == true
|
44
|
+
end
|
45
|
+
|
46
|
+
specify('should default to a general context if unspecified') do
|
47
|
+
class Cow
|
48
|
+
validations.clear!
|
49
|
+
validates_presence_of :name, :age
|
50
|
+
end
|
51
|
+
|
52
|
+
rhonda = Cow.new
|
53
|
+
rhonda.valid?.should == false
|
54
|
+
|
55
|
+
rhonda.errors.full_messages.should include('Age must not be blank')
|
56
|
+
rhonda.errors.full_messages.should include('Name must not be blank')
|
57
|
+
|
58
|
+
rhonda.name = 'Rhonda'
|
59
|
+
rhonda.age = 44
|
60
|
+
rhonda.valid?.should == true
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|