ricordami 0.0.1

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.
Files changed (62) hide show
  1. data/CHANGELOG.md +13 -0
  2. data/ISSUES.md +0 -0
  3. data/MIT-LICENSE +21 -0
  4. data/README.md +454 -0
  5. data/TODO.md +21 -0
  6. data/examples/calls.rb +64 -0
  7. data/examples/singers.rb +42 -0
  8. data/lib/ricordami/attribute.rb +52 -0
  9. data/lib/ricordami/can_be_queried.rb +133 -0
  10. data/lib/ricordami/can_be_validated.rb +27 -0
  11. data/lib/ricordami/can_have_relationships.rb +152 -0
  12. data/lib/ricordami/configuration.rb +39 -0
  13. data/lib/ricordami/connection.rb +22 -0
  14. data/lib/ricordami/exceptions.rb +14 -0
  15. data/lib/ricordami/has_attributes.rb +159 -0
  16. data/lib/ricordami/has_indices.rb +105 -0
  17. data/lib/ricordami/is_lockable.rb +52 -0
  18. data/lib/ricordami/is_persisted.rb +123 -0
  19. data/lib/ricordami/is_retrievable.rb +35 -0
  20. data/lib/ricordami/key_namer.rb +63 -0
  21. data/lib/ricordami/model.rb +29 -0
  22. data/lib/ricordami/query.rb +68 -0
  23. data/lib/ricordami/relationship.rb +40 -0
  24. data/lib/ricordami/unique_index.rb +59 -0
  25. data/lib/ricordami/unique_validator.rb +21 -0
  26. data/lib/ricordami/value_index.rb +26 -0
  27. data/lib/ricordami/version.rb +3 -0
  28. data/lib/ricordami.rb +26 -0
  29. data/spec/acceptance/manage_relationships_spec.rb +42 -0
  30. data/spec/acceptance/model_with_validation_spec.rb +78 -0
  31. data/spec/acceptance/query_model_spec.rb +93 -0
  32. data/spec/acceptance_helper.rb +2 -0
  33. data/spec/ricordami/attribute_spec.rb +113 -0
  34. data/spec/ricordami/can_be_queried_spec.rb +254 -0
  35. data/spec/ricordami/can_be_validated_spec.rb +115 -0
  36. data/spec/ricordami/can_have_relationships_spec.rb +255 -0
  37. data/spec/ricordami/configuration_spec.rb +45 -0
  38. data/spec/ricordami/connection_spec.rb +25 -0
  39. data/spec/ricordami/exceptions_spec.rb +43 -0
  40. data/spec/ricordami/has_attributes_spec.rb +266 -0
  41. data/spec/ricordami/has_indices_spec.rb +73 -0
  42. data/spec/ricordami/is_lockable_spec.rb +45 -0
  43. data/spec/ricordami/is_persisted_spec.rb +186 -0
  44. data/spec/ricordami/is_retrievable_spec.rb +55 -0
  45. data/spec/ricordami/key_namer_spec.rb +56 -0
  46. data/spec/ricordami/model_spec.rb +65 -0
  47. data/spec/ricordami/query_spec.rb +156 -0
  48. data/spec/ricordami/relationship_spec.rb +123 -0
  49. data/spec/ricordami/unique_index_spec.rb +87 -0
  50. data/spec/ricordami/unique_validator_spec.rb +41 -0
  51. data/spec/ricordami/value_index_spec.rb +40 -0
  52. data/spec/spec_helper.rb +29 -0
  53. data/spec/support/constants.rb +43 -0
  54. data/spec/support/db_manager.rb +18 -0
  55. data/test/bin/data_loader.rb +107 -0
  56. data/test/data/domains.txt +462 -0
  57. data/test/data/first_names.txt +1220 -0
  58. data/test/data/last_names.txt +1028 -0
  59. data/test/data/people_100_000.csv.bz2 +0 -0
  60. data/test/data/people_10_000.csv.bz2 +0 -0
  61. data/test/data/people_1_000_000.csv.bz2 +0 -0
  62. metadata +258 -0
@@ -0,0 +1,93 @@
1
+ require "acceptance_helper"
2
+ require "ricordami/can_be_queried"
3
+
4
+ class Singer
5
+ include Ricordami::Model
6
+ include Ricordami::CanBeQueried
7
+
8
+ attribute :username
9
+ attribute :email
10
+ attribute :first_name
11
+ attribute :last_name
12
+ attribute :deceased, :default => "false", :indexed => :value
13
+
14
+ index :unique => :username, :get_by => true
15
+ end
16
+
17
+ feature "Query model" do
18
+ def create_12_singers
19
+ [
20
+ ["Serge", "Gainsbourg"], ["Alain", "Bashung"], ["Benjamin", "Biolay"],
21
+ ["Charles", "Aznavour"], ["Yves", "Montand", true], ["Nino", "Ferrer", true],
22
+ ["Johnny", "Hallyday"], ["David", "Guetta"], ["Bruno", "Benabar"],
23
+ ["Alain", "Souchon"], ["Jacques", "Dutronc"], ["Georges", "Brasens"]
24
+ ].each do |first, last, deceased|
25
+ Singer.create(:username => (first + last[0..0]).downcase, :first_name => first,
26
+ :last_name => last, :email => "#{first.downcase}@#{last.downcase}.fr",
27
+ :deceased => deceased ? "true" : "false").should be_persisted
28
+ end
29
+ end
30
+
31
+ scenario "retrieve models" do
32
+ Singer.create(:username => "lucien", :email => "serge@gainsbourg.com",
33
+ :first_name => "Serge", :last_name => "Gainsbourg")
34
+ Singer.get_by_username("lucien").id.should == "1"
35
+
36
+ Singer.create(:username => "bashung", :email => "alain@bashung.com",
37
+ :first_name => "Alain", :last_name => "Bashung")
38
+ Singer.get_by_username("bashung").id.should == "2"
39
+
40
+ Singer.create(:username => "ben", :email => "benjamin@biolay.com",
41
+ :first_name => "Benjamin", :last_name => "Biolay")
42
+ Singer.get_by_username("ben").id.should == "3"
43
+
44
+ Singer.count.should == 3
45
+ Singer["1"].email.should == "serge@gainsbourg.com"
46
+
47
+ Singer.get_by_username("bashung").first_name.should == "Alain"
48
+ end
49
+
50
+ scenario "finding models using basic queries" do
51
+ Singer.create(:username => "ben", :email => "benjamin@biolay.com",
52
+ :first_name => "Benjamin", :last_name => "Biolay", :deceased => "false")
53
+ Singer.create(:username => "lucien", :email => "serge@gainsbourg.com",
54
+ :first_name => "Serge", :last_name => "Gainsbourg", :deceased => "true")
55
+ Singer.create(:username => "bashung", :email => "alain@bashung.com",
56
+ :first_name => "Alain", :last_name => "Bashung", :deceased => "true")
57
+
58
+ deceased = Singer.and(:deceased => "true").all
59
+ deceased.map(&:username).should =~ %w(lucien bashung)
60
+ q = Singer.where(:deceased => "true")
61
+ q.sort(:first_name, :asc_alpha).first.email.should == "alain@bashung.com"
62
+ q.sort(:first_name, :desc_alpha).last.email.should == "alain@bashung.com"
63
+ Singer.not(:deceased => true).map(&:username).should == ["ben"]
64
+ first = Singer.first.username
65
+ last = Singer.last.username
66
+ rand = Singer.rand.username
67
+ first.should_not == last
68
+ Singer.all.map(&:username).should include(first)
69
+ Singer.all.map(&:username).should include(last)
70
+ Singer.all.map(&:username).should include(rand)
71
+ end
72
+
73
+ scenario "paginate list of models" do
74
+ create_12_singers
75
+
76
+ Singer.paginate(:page => 1, :per_page => 5).should have(5).singers
77
+ Singer.paginate(:page => 2, :per_page => 5).should have(5).singers
78
+ Singer.paginate(:page => 3, :per_page => 5).should have(2).singers
79
+ Singer.paginate(:page => 4, :per_page => 5).should have(0).singers
80
+
81
+ page1 = Singer.sort(:last_name, :asc_alpha).paginate(:page => 1, :per_page => 5)
82
+ page1.map(&:last_name).should == ["Aznavour", "Bashung", "Benabar", "Biolay", "Brasens"]
83
+ page3 = Singer.sort(:last_name, :asc_alpha).paginate(:page => 3, :per_page => 5)
84
+ page3.map(&:last_name).should == ["Montand", "Souchon"]
85
+ end
86
+
87
+ scenario "query using #and, #any and #not filters" do
88
+ Singer.index :value => :first_name
89
+ create_12_singers
90
+
91
+ Singer.where(:first_name => "Alain").all.map(&:last_name).should =~ %w(Bashung Souchon)
92
+ end
93
+ end
@@ -0,0 +1,2 @@
1
+ require File.expand_path("../spec_helper", __FILE__)
2
+ require "steak"
@@ -0,0 +1,113 @@
1
+ require "spec_helper"
2
+
3
+ describe Ricordami::Attribute do
4
+ subject { Ricordami::Attribute }
5
+
6
+ describe "an instance" do
7
+ it "has a name" do
8
+ attribute = subject.new(:singer)
9
+ attribute.name.should == :singer
10
+ end
11
+
12
+ it "accepts a string for its name" do
13
+ attribute = subject.new("string")
14
+ attribute.name.should == :string
15
+ end
16
+
17
+ it "accepts also a symbol for its name" do
18
+ attribute = subject.new(:string)
19
+ attribute.name.should == :string
20
+ end
21
+
22
+ it "has an option :default for a default value" do
23
+ attribute = subject.new(:georges, :default => "jungle")
24
+ attribute.default_value.should == "jungle"
25
+ end
26
+
27
+ it "doesn't have a default value if :default is not specified" do
28
+ subject.new(:no_defaults).default_value.should be_nil
29
+ end
30
+
31
+ it "allows to use a block as a default value for dynamic values" do
32
+ i = 0
33
+ attribute = subject.new(:sequence, :default => Proc.new { i += 1 })
34
+ attribute.default_value.should == 1
35
+ attribute.default_value.should == 2
36
+ attribute.default_value.should == 3
37
+ end
38
+
39
+ it "retuns if a default value is set with #default_value?" do
40
+ subject.new(:blah, :default => "1").default_value?.should be_true
41
+ without = subject.new(:foo).default_value?.should be_false
42
+ end
43
+
44
+ it "has an option :read_only when the attribute value can only be set once" do
45
+ attribute = subject.new(:georges, :read_only => true)
46
+ attribute.should be_read_only
47
+ end
48
+
49
+ it "its value can be set more than once when :read_only is not set" do
50
+ subject.new(:not_read_only).should_not be_read_only
51
+ end
52
+
53
+ it "has an option :indexed to index the attribute as unique" do
54
+ attribute = subject.new(:georges, :indexed => :unique)
55
+ attribute.indexed.should == :unique
56
+ end
57
+
58
+ it "has an option :indexed to index the attribute by value" do
59
+ attribute = subject.new(:georges, :indexed => :value)
60
+ attribute.indexed.should == :value
61
+ end
62
+
63
+ it "raises an error if :indexed is not :unique or :value" do
64
+ lambda {
65
+ subject.new(:georges, :indexed => :blah)
66
+ }.should raise_error(Ricordami::InvalidIndexDefinition)
67
+ lambda {
68
+ subject.new(:georges)
69
+ }.should_not raise_error
70
+ end
71
+
72
+ it "its value can be used for queries when :indexed is not set" do
73
+ subject.new(:not_indexed).should_not be_indexed
74
+ end
75
+
76
+ it "has an option :initial for an initial value set when saved the first time" do
77
+ attribute = subject.new(:id, :initial => "123")
78
+ attribute.initial_value.should == "123"
79
+ end
80
+
81
+ it "doesn't have an initial value if :initial is not specified" do
82
+ subject.new(:no_initials).initial_value.should be_nil
83
+ end
84
+
85
+ it "allows to use a block as an initial value for dynamic values" do
86
+ i = 1
87
+ attribute = subject.new(:id, :initial => Proc.new { i *= 2 })
88
+ attribute.initial_value.should == 2
89
+ attribute.initial_value.should == 4
90
+ attribute.initial_value.should == 8
91
+ end
92
+
93
+ it "retuns if an initial value is set with #initial_value?" do
94
+ subject.new(:id, :initial => "1").initial_value?.should be_true
95
+ without = subject.new(:foo).initial_value?.should be_false
96
+ end
97
+
98
+ it "can spefify the attribute type with :type option" do
99
+ attribute = subject.new(:id, :type => :integer)
100
+ attribute.type.should == :integer
101
+ end
102
+
103
+ it "has a default type :string if not specified" do
104
+ subject.new(:name).type.should == :string
105
+ end
106
+
107
+ it "returns the converter method with #converter" do
108
+ subject.new(:name, :type => :string).converter.should == :to_s
109
+ subject.new(:name, :type => :integer).converter.should == :to_i
110
+ subject.new(:name, :type => :float).converter.should == :to_f
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,254 @@
1
+ require "spec_helper"
2
+ require "ricordami/can_be_queried"
3
+
4
+ describe Ricordami::CanBeQueried do
5
+ uses_constants("Customer")
6
+
7
+ before(:each) do
8
+ Customer.send(:include, Ricordami::CanBeQueried)
9
+ Customer.attribute :country, :indexed => :value
10
+ Customer.attribute :sex, :indexed => :value
11
+ Customer.attribute :name, :indexed => :value
12
+ Customer.attribute :kind, :indexed => :value
13
+ Customer.attribute :no_index
14
+ end
15
+
16
+ describe "building queries" do
17
+ describe "#and" do
18
+ it "returns a new query" do
19
+ query = Customer.and
20
+ query.should be_a(Ricordami::Query)
21
+ end
22
+
23
+ it "passes self as the query runner to the query" do
24
+ query = Customer.and
25
+ query.runner.should == Customer
26
+ end
27
+
28
+ it "delegates #and to the new query" do
29
+ query = Customer.and(:key => "value")
30
+ query.expressions.should == [[:and, {:key => "value"}]]
31
+ end
32
+ end
33
+
34
+ describe "#not" do
35
+ it "returns a new query" do
36
+ query = Customer.not
37
+ query.should be_a(Ricordami::Query)
38
+ end
39
+
40
+ it "delegates #not to the new query" do
41
+ query = Customer.not(:key => "value")
42
+ query.expressions.should == [[:not, {:key => "value"}]]
43
+ end
44
+ end
45
+
46
+ describe "#any" do
47
+ it "returns a new query" do
48
+ query = Customer.any
49
+ query.should be_a(Ricordami::Query)
50
+ end
51
+
52
+ it "delegates #any to the new query" do
53
+ query = Customer.any(:key => "value")
54
+ query.expressions.should == [[:any, {:key => "value"}]]
55
+ end
56
+ end
57
+
58
+ describe "#sort" do
59
+ it "returns a new query" do
60
+ query = Customer.sort(:sex)
61
+ query.should be_a(Ricordami::Query)
62
+ end
63
+
64
+ it "delegates #sort to the new query" do
65
+ query = Customer.sort(:sex, :desc_alpha)
66
+ query.sort_by.should == :sex
67
+ query.sort_dir.should == :desc_alpha
68
+ end
69
+ end
70
+ end
71
+
72
+ describe "running queries" do
73
+ before(:each) do
74
+ Customer.create(:name => "Zhanna", :sex => "F", :country => "Latvia", :kind => "human")
75
+ Customer.create(:name => "Mathieu", :sex => "M", :country => "France", :kind => "human")
76
+ Customer.create(:name => "Sophie", :sex => "F", :country => "USA", :kind => "human")
77
+ Customer.create(:name => "Brioche", :sex => "F", :country => "USA", :kind => "dog")
78
+ end
79
+
80
+ describe ":and" do
81
+ it "raises an error if there's no value index for one of the conditions" do
82
+ lambda {
83
+ Customer.and(:no_index => "Blah").all
84
+ }.should raise_error(Ricordami::MissingIndex)
85
+ end
86
+
87
+ it "returns all entries if no conditions where passed" do
88
+ Customer.and.all.map(&:name).should =~ %w(Zhanna Mathieu Sophie Brioche)
89
+ end
90
+
91
+ it "returns the models found with #all (1 condition, 1 result)" do
92
+ Customer.index :value => :name
93
+ found = Customer.and(:name => "Zhanna").all
94
+ found.map(&:name).should == ["Zhanna"]
95
+ end
96
+
97
+ it "returns the models found with #all (2 conditions, 2 results)" do
98
+ found = Customer.and(:country => "USA", :sex => "F").all
99
+ found.map(&:name).should =~ ["Sophie", "Brioche"]
100
+ end
101
+
102
+ it "returns the models found with #all for a composed query" do
103
+ found = Customer.and(:country => "USA").and(:sex => "F").all
104
+ found.map(&:name).should =~ ["Sophie", "Brioche"]
105
+ end
106
+
107
+ it "can use #where instead of #and" do
108
+ found = Customer.where(:country => "USA").and(:sex => "F").all
109
+ found.map(&:name).should =~ ["Sophie", "Brioche"]
110
+ end
111
+
112
+ it "doesn't require #all if another method call is chained" do
113
+ Customer.where(:country => "USA").and(:sex => "F").map(&:name).should =~ ["Sophie", "Brioche"]
114
+ end
115
+ end
116
+
117
+ describe ":any" do
118
+ it "raises an error if there's no value index for one of the conditions" do
119
+ lambda {
120
+ Customer.any(:no_index => "Blah").all
121
+ }.should raise_error(Ricordami::MissingIndex)
122
+ end
123
+
124
+ it "returns all entries if no conditions where passed" do
125
+ Customer.any.all.map(&:name).should =~ %w(Zhanna Mathieu Sophie Brioche)
126
+ end
127
+
128
+ it "returns the models found with #all (1 condition, 1 result)" do
129
+ Customer.index :value => :name
130
+ found = Customer.any(:name => "Zhanna").all
131
+ found.map(&:name).should == ["Zhanna"]
132
+ end
133
+
134
+ it "returns the models found with #all (2 conditions, 3 results)" do
135
+ found = Customer.any(:country => "USA", :sex => "F").all
136
+ found.map(&:name).should =~ ["Sophie", "Brioche", "Zhanna"]
137
+ end
138
+
139
+ it "returns the models found with #all for a composed query" do
140
+ found = Customer.where(:country => "USA").any(:name => "Sophie", :kind => "dog").all
141
+ found.map(&:name).should =~ ["Sophie", "Brioche"]
142
+ found = Customer.where(:country => "USA").any(:name => "Sophie", :kind => "human").all
143
+ found.map(&:name).should == ["Sophie"]
144
+ end
145
+
146
+ it "doesn't require #all if another method call is chained" do
147
+ Customer.any(:country => "USA", :sex => "F").map(&:name).should =~ ["Sophie", "Brioche", "Zhanna"]
148
+ end
149
+ end
150
+
151
+ describe ":not" do
152
+ it "raises an error if there's no value index for one of the conditions" do
153
+ lambda {
154
+ Customer.not(:no_index => "Blah").all
155
+ }.should raise_error(Ricordami::MissingIndex)
156
+ end
157
+
158
+ it "returns all entries if no conditions where passed" do
159
+ Customer.not.all.map(&:name).should =~ %w(Zhanna Mathieu Sophie Brioche)
160
+ end
161
+
162
+ it "returns the models found with #all (1 condition, 1 result)" do
163
+ Customer.index :value => :name
164
+ found = Customer.not(:name => "Zhanna").all
165
+ found.map(&:name).should =~ ["Sophie", "Brioche", "Mathieu"]
166
+ end
167
+
168
+ it "returns the models found with #all (2 conditions, 1 result)" do
169
+ found = Customer.not(:country => "USA", :sex => "F").all
170
+ found.map(&:name).should == ["Mathieu"]
171
+ end
172
+
173
+ it "returns the models found with #all for a composed query" do
174
+ found = Customer.where(:country => "USA").not(:name => "Sophie", :kind => "dog").all
175
+ found.map(&:name).should be_empty
176
+ found = Customer.where(:country => "USA").not(:name => "Sophie", :kind => "human").all
177
+ found.map(&:name).should == ["Brioche"]
178
+ end
179
+
180
+ it "doesn't require #all if another method call is chained" do
181
+ Customer.not(:country => "USA", :sex => "F").map(&:name).should == ["Mathieu"]
182
+ end
183
+ end
184
+ end
185
+
186
+ describe "sorting result" do
187
+ uses_constants("Student")
188
+
189
+ before(:each) do
190
+ Student.send(:include, Ricordami::CanBeQueried)
191
+ Student.attribute :name, :indexed => :value
192
+ Student.attribute :grade, :indexed => :value
193
+ Student.attribute :school, :indexed => :value
194
+ [["Zhanna", 12], ["Sophie", 19],
195
+ ["Brioche", 4], ["Mathieu", 15]].each do |name, grade|
196
+ Student.create(:name => name, :grade => grade, :school => "Lajoo")
197
+ end
198
+ end
199
+
200
+ it "can sort the result alphanumerically with #sort" do
201
+ result = Student.where(:school => "Lajoo").sort(:name, :asc_alpha)
202
+ result.map(&:name).should == %w(Brioche Mathieu Sophie Zhanna)
203
+ end
204
+
205
+ it "can sort the result numerically with #sort" do
206
+ result = Student.where(:school => "Lajoo").sort(:grade, :asc_num)
207
+ result.map(&:name).should == %w(Brioche Zhanna Mathieu Sophie)
208
+ end
209
+
210
+ it "defaults to sorting ascending / alphanumerically" do
211
+ result = Student.where(:school => "Lajoo").sort(:name)
212
+ result.map(&:name).should == %w(Brioche Mathieu Sophie Zhanna)
213
+ end
214
+ end
215
+
216
+ describe "fetching result" do
217
+ before(:each) do
218
+ Student.send(:include, Ricordami::CanBeQueried)
219
+ Student.attribute :name, :indexed => :value
220
+ Student.attribute :grade, :indexed => :value
221
+ Student.attribute :school, :indexed => :value
222
+ [["Zhanna", 12], ["Sophie", 19],
223
+ ["Brioche", 4], ["Mathieu", 15]].each do |name, grade|
224
+ Student.create(:name => name, :grade => grade, :school => "Lajoo")
225
+ end
226
+ @query = Student.where(:school => "Lajoo").sort(:name)
227
+ end
228
+ let(:query) { @query }
229
+
230
+ it "fetches all the results with #all" do
231
+ query.all.map(&:name).should == %w(Brioche Mathieu Sophie Zhanna)
232
+ end
233
+
234
+ it "fetches the requested page of the results with #paginate" do
235
+ fetched = query.paginate(:page => 1, :per_page => 2)
236
+ fetched.map(&:name).should == %w(Brioche Mathieu)
237
+ fetched = query.paginate(:page => 2, :per_page => 2)
238
+ fetched.map(&:name).should == %w(Sophie Zhanna)
239
+ query.paginate(:page => 3, :per_page => 2).should be_empty
240
+ end
241
+
242
+ it "fetches the first instance with #first" do
243
+ query.first.name.should == "Brioche"
244
+ end
245
+
246
+ it "fetches the last instance with #last" do
247
+ query.last.name.should == "Zhanna"
248
+ end
249
+
250
+ it "fetches a random instance with #rand" do
251
+ %w(Brioche Mathieu Sophie Zhanna).should include(query.rand.name)
252
+ end
253
+ end
254
+ end
@@ -0,0 +1,115 @@
1
+ require "spec_helper"
2
+ require "ricordami/can_be_validated"
3
+
4
+ describe Ricordami::CanBeValidated do
5
+ describe "Active Model validations" do
6
+ uses_constants("Call")
7
+
8
+ before(:each) do
9
+ Call.class_eval do
10
+ include Ricordami::CanBeValidated
11
+ attribute :ani
12
+ attribute :dnis
13
+ validates_presence_of :ani
14
+ validates_numericality_of :dnis, :allow_blank => true, :message => "is not numeric"
15
+ validate :cant_call_iself
16
+
17
+ private
18
+ def cant_call_iself
19
+ return true if ani.blank?
20
+ errors.add(:ani, "can't be the same as dnis") if ani == dnis
21
+ end
22
+ end
23
+ end
24
+
25
+ it "can validate the presence of an attribute value" do
26
+ call = Call.new
27
+ call.should_not be_valid
28
+ call.should have(1).errors
29
+ call.errors[:ani].should == ["can't be blank"]
30
+ end
31
+
32
+ it "can validate that a field is numeric" do
33
+ call = Call.new(:ani => "123", :dnis => "nope")
34
+ call.should_not be_valid
35
+ call.should have(1).errors
36
+ call.errors[:dnis].should == ["is not numeric"]
37
+ end
38
+
39
+ it "can do custom validation" do
40
+ call = Call.new(:ani => "123", :dnis => "123")
41
+ call.should_not be_valid
42
+ call.should have(1).errors
43
+ call.errors[:ani].should == ["can't be the same as dnis"]
44
+ end
45
+
46
+ it "can't validate a model that was deleted" do
47
+ call = Call.create(:ani => "123", :dnis => "456")
48
+ call.delete
49
+ lambda { call.valid? }.should raise_error(Ricordami::ModelHasBeenDeleted)
50
+ end
51
+ end
52
+
53
+ describe "#save" do
54
+ uses_constants("Post")
55
+
56
+ before(:each) do
57
+ Post.class_eval do
58
+ include Ricordami::CanBeValidated
59
+ attribute :title
60
+ validates_presence_of :title
61
+ end
62
+ end
63
+
64
+ it "can't #save if it is not valid" do
65
+ post = Post.new
66
+ post.save.should be_false
67
+ end
68
+
69
+ it "can save if it is not valid but passed :validate => false" do
70
+ Post.new.save(:validate => false).should be_true
71
+ end
72
+ end
73
+
74
+ describe "validate attribute uniqueness" do
75
+ uses_constants("User")
76
+
77
+ before(:each) do
78
+ User.class_eval do
79
+ include Ricordami::CanBeValidated
80
+ attribute :username, :read_only => true
81
+ attribute :wife
82
+ validates_uniqueness_of :username
83
+ end
84
+ end
85
+
86
+ it "is valid if no other instance uses the same attribute value" do
87
+ user = User.new(:username => "Serge Gainsbourg")
88
+ user.should be_valid
89
+ end
90
+
91
+ it "is not valid if another instance uses the same attribute value" do
92
+ serge = User.new(:username => "Gainsbourg")
93
+ serge.save.should be_true
94
+ serge.should be_valid
95
+
96
+ usurpateur = User.new(:username => "Gainsbourg")
97
+ usurpateur.should_not be_valid
98
+ usurpateur.should have(1).error
99
+ usurpateur.errors[:username].should == ["is already used"]
100
+ end
101
+
102
+ it "allows to validate the uniqueness of an attribute that can be changed" do
103
+ User.validates_uniqueness_of(:wife)
104
+ User.create(:id => "serge", :username => "Gainsbourg", :wife => "Rita")
105
+ serge = User["serge"]
106
+ serge.should be_valid
107
+ serge.save
108
+
109
+ fred = User.new(:username => "Chichin", :wife => "Rita")
110
+ fred.should_not be_valid
111
+ fred.should have(1).error
112
+ fred.errors[:wife].should == ["is already used"]
113
+ end
114
+ end
115
+ end