model_attribute 0.0.1 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,14 @@
1
+ module ModelAttribute
2
+ class InvalidAttributeNameError < StandardError
3
+ def initialize(attribute_name)
4
+ super "Invalid attribute name #{attribute_name.inspect}"
5
+ end
6
+ end
7
+
8
+ class UnsupportedTypeError < StandardError
9
+ def initialize(type)
10
+ types_list = ModelAttribute::SUPPORTED_TYPES.map(&:inspect).join(', ')
11
+ super "Unsupported type #{type.inspect}. Must be one of #{types_list}."
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,27 @@
1
+ module ModelAttribute
2
+ module Json
3
+ class << self
4
+ def valid?(value)
5
+ (value == nil ||
6
+ value == true ||
7
+ value == false ||
8
+ value.is_a?(Numeric) ||
9
+ value.is_a?(String) ||
10
+ (value.is_a?(Array) && valid_array?(value)) ||
11
+ (value.is_a?(Hash) && valid_hash?(value) ))
12
+ end
13
+
14
+ private
15
+
16
+ def valid_array?(array)
17
+ array.all? { |value| valid?(value) }
18
+ end
19
+
20
+ def valid_hash?(hash)
21
+ hash.all? do |key, value|
22
+ key.is_a?(String) && valid?(value)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -1,3 +1,3 @@
1
1
  module ModelAttribute
2
- VERSION = "0.0.1"
2
+ VERSION = "2.0.0"
3
3
  end
@@ -8,7 +8,7 @@ Gem::Specification.new do |spec|
8
8
  spec.version = ModelAttribute::VERSION
9
9
  spec.authors = ["David Waller"]
10
10
  spec.email = ["dwaller@yammer-inc.com"]
11
- spec.summary = %q{Placeholder while clearing gem release with legal}
11
+ spec.summary = %q{Attributes for non-ActiveRecord models}
12
12
  spec.homepage = ""
13
13
  spec.license = "MIT"
14
14
 
@@ -17,6 +17,11 @@ Gem::Specification.new do |spec|
17
17
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18
18
  spec.require_paths = ["lib"]
19
19
 
20
- spec.add_development_dependency "bundler", "~> 1.7"
21
- spec.add_development_dependency "rake", "~> 10.0"
20
+ spec.add_development_dependency "bundler", "~> 1.7"
21
+ spec.add_development_dependency "rake", "~> 10.0"
22
+ spec.add_development_dependency "rspec", "~> 3.1"
23
+ spec.add_development_dependency "rspec-nc", "~> 0.2"
24
+ spec.add_development_dependency "guard", "~> 2.8"
25
+ spec.add_development_dependency "guard-rspec", "~> 4.3"
26
+ spec.add_development_dependency "pry-debugger"
22
27
  end
@@ -0,0 +1,66 @@
1
+ require 'benchmark'
2
+ $LOAD_PATH << "lib"
3
+
4
+ Benchmark.bm(41) do |bm|
5
+ bm.report("Virtus load") do
6
+ require 'virtus'
7
+
8
+ class VirtusUser
9
+ include Virtus.model
10
+ attribute :id, Integer
11
+ attribute :name, String
12
+ attribute :paid, Boolean
13
+ attribute :updated_at, DateTime
14
+ end
15
+ end
16
+
17
+ bm.report("ModelAttribute load") do
18
+ require_relative 'lib/model_attribute'
19
+
20
+ class ModelAttributeUser
21
+ extend ModelAttribute
22
+ attribute :id, :integer
23
+ attribute :name, :string
24
+ attribute :paid, :boolean
25
+ attribute :updated_at, :time
26
+ end
27
+ end
28
+ end
29
+
30
+ Benchmark.bm(41) do |bm|
31
+ vu = VirtusUser.new
32
+ mau = ModelAttributeUser.new
33
+ bm.report("Virtus assign integer") { 10_000.times { vu.id = rand(100_000) } }
34
+ bm.report("ModelAttribute assign integer") { 10_000.times { mau.id = rand(100_000) } }
35
+ bm.report("Virtus assign integer from string") { 10_000.times { vu.id = rand(100_000).to_s } }
36
+ bm.report("ModelAttribute assign integer from string") { 10_000.times { mau.id = rand(100_000).to_s } }
37
+ bm.report("Virtus assign time") { 10_000.times { vu.updated_at = Time.now } }
38
+ bm.report("ModelAttribute assign time") { 10_000.times { mau.updated_at = Time.now } }
39
+ bm.report("Virtus assign DateTime") { 10_000.times { vu.updated_at = DateTime.now } }
40
+ bm.report("ModelAttribute assign DateTime") { 10_000.times { mau.updated_at = DateTime.now } }
41
+ bm.report("Virtus assign time from epoch") { 10_000.times { vu.updated_at = Time.now.to_f } }
42
+ bm.report("ModelAttribute assign time from epoch") { 10_000.times { mau.updated_at = Time.now.to_f } }
43
+ bm.report("Virtus assign time from string") { 10_000.times { vu.updated_at = "2014-12-25 06:00:00" } }
44
+ bm.report("ModelAttribute assign time from string") { 10_000.times { mau.updated_at = "2014-12-25 06:00:00" } }
45
+ end
46
+
47
+ __END__
48
+ $ ruby -v
49
+ ruby 1.9.3p545 (2014-02-24 revision 45159) [x86_64-darwin13.3.0]
50
+ $ ruby performance_comparison.rb
51
+ user system total real
52
+ Virtus load 0.120000 0.040000 0.160000 ( 0.207931)
53
+ ModelAttribute load 0.020000 0.010000 0.030000 ( 0.027237)
54
+ user system total real
55
+ Virtus assign integer 0.010000 0.000000 0.010000 ( 0.013906)
56
+ ModelAttribute assign integer 0.030000 0.000000 0.030000 ( 0.033674)
57
+ Virtus assign integer from string 0.170000 0.000000 0.170000 ( 0.171892)
58
+ ModelAttribute assign integer from string 0.050000 0.000000 0.050000 ( 0.042726)
59
+ Virtus assign time 0.080000 0.000000 0.080000 ( 0.089792)
60
+ ModelAttribute assign time 0.060000 0.000000 0.060000 ( 0.057887)
61
+ Virtus assign DateTime 0.030000 0.000000 0.030000 ( 0.026447)
62
+ ModelAttribute assign DateTime 0.200000 0.000000 0.200000 ( 0.204524)
63
+ Virtus assign time from epoch 0.230000 0.010000 0.240000 ( 0.225557)
64
+ ModelAttribute assign time from epoch 0.110000 0.000000 0.110000 ( 0.113315)
65
+ Virtus assign time from string 0.260000 0.000000 0.260000 ( 0.264467)
66
+ ModelAttribute assign time from string 0.450000 0.000000 0.450000 ( 0.444686)
@@ -0,0 +1,611 @@
1
+ class User
2
+ extend ModelAttribute
3
+ attribute :id, :integer
4
+ attribute :paid, :boolean
5
+ attribute :name, :string
6
+ attribute :created_at, :time
7
+ attribute :profile, :json
8
+
9
+ def initialize(attributes = {})
10
+ set_attributes(attributes)
11
+ end
12
+ end
13
+
14
+ class UserWithoutId
15
+ extend ModelAttribute
16
+ attribute :paid, :boolean
17
+ attribute :name, :string
18
+ attribute :created_at, :time
19
+
20
+ def initialize(attributes = {})
21
+ set_attributes(attributes)
22
+ end
23
+ end
24
+
25
+ RSpec.describe "a class using ModelAttribute" do
26
+ describe ".attributes" do
27
+ it "returns an array of attribute names as symbols" do
28
+ expect(User.attributes).to eq([:id, :paid, :name, :created_at, :profile])
29
+ end
30
+ end
31
+
32
+ describe "defining an attribute with an invalid type" do
33
+ it "raises an error" do
34
+ expect do
35
+ User.attribute :address, :custom_type
36
+ end.to raise_error(ModelAttribute::UnsupportedTypeError,
37
+ "Unsupported type :custom_type. " +
38
+ "Must be one of :integer, :boolean, :string, :time, :json.")
39
+ end
40
+ end
41
+
42
+ describe "an instance of the class" do
43
+ let(:user) { User.new }
44
+
45
+ describe "an integer attribute (id)" do
46
+ it "is nil when unset" do
47
+ expect(user.id).to be_nil
48
+ end
49
+
50
+ it "stores an integer" do
51
+ user.id = 3
52
+ expect(user.id).to eq(3)
53
+ end
54
+
55
+ it "stores an integer passed as a float" do
56
+ user.id = 3.0
57
+ expect(user.id).to eq(3)
58
+ end
59
+
60
+ it "raises when passed a float with non-zero decimal part" do
61
+ expect { user.id = 3.3 }.to raise_error
62
+ end
63
+
64
+ it "parses an integer string" do
65
+ user.id = '3'
66
+ expect(user.id).to eq(3)
67
+ end
68
+
69
+ it "raises if passed a string it can't parse" do
70
+ expect { user.id = '3a' }.to raise_error
71
+ end
72
+
73
+ it "stores nil" do
74
+ user.id = 3
75
+ user.id = nil
76
+ expect(user.id).to be_nil
77
+ end
78
+
79
+ it "does not provide an id? method" do
80
+ expect(user).to_not respond_to(:id?)
81
+ expect { user.id? }.to raise_error(NoMethodError)
82
+ end
83
+ end
84
+
85
+ describe "a boolean attribute (paid)" do
86
+ it "is nil when unset" do
87
+ expect(user.paid).to be_nil
88
+ end
89
+
90
+ it "stores true" do
91
+ user.paid = true
92
+ expect(user.paid).to eq(true)
93
+ end
94
+
95
+ it "stores false" do
96
+ user.paid = false
97
+ expect(user.paid).to eq(false)
98
+ end
99
+
100
+ it "parses 't' as true" do
101
+ user.paid = 't'
102
+ expect(user.paid).to eq(true)
103
+ end
104
+
105
+ it "parses 'f' as false" do
106
+ user.paid = 'f'
107
+ expect(user.paid).to eq(false)
108
+ end
109
+
110
+ it "raises if passed a string it can't parse" do
111
+ expect { user.paid = '3a' }.to raise_error
112
+ end
113
+
114
+ it "stores nil" do
115
+ user.paid = true
116
+ user.paid = nil
117
+ expect(user.paid).to be_nil
118
+ end
119
+
120
+ describe "#paid?" do
121
+ it "returns false when unset" do
122
+ expect(user.paid?).to eq(false)
123
+ end
124
+
125
+ it "returns false for false attributes" do
126
+ user.paid = false
127
+ expect(user.paid?).to eq(false)
128
+ end
129
+
130
+ it "returns true for true attributes" do
131
+ user.paid = true
132
+ expect(user.paid?).to eq(true)
133
+ end
134
+ end
135
+ end
136
+
137
+ describe "a string attribute (name)" do
138
+ it "is nil when unset" do
139
+ expect(user.name).to be_nil
140
+ end
141
+
142
+ it "stores a string" do
143
+ user.name = 'Fred'
144
+ expect(user.name).to eq('Fred')
145
+ end
146
+
147
+ it "casts an integer to a string" do
148
+ user.name = 3
149
+ expect(user.name).to eq('3')
150
+ end
151
+
152
+ it "stores nil" do
153
+ user.name = 'Fred'
154
+ user.name = nil
155
+ expect(user.name).to be_nil
156
+ end
157
+
158
+ it "does not provide a name? method" do
159
+ expect(user).to_not respond_to(:name?)
160
+ expect { user.name? }.to raise_error(NoMethodError)
161
+ end
162
+ end
163
+
164
+ describe "a time attribute (created_at)" do
165
+ let(:now_time) { Time.now }
166
+
167
+ it "is nil when unset" do
168
+ expect(user.created_at).to be_nil
169
+ end
170
+
171
+ it "stores a Time object" do
172
+ user.created_at = now_time
173
+ expect(user.created_at).to eq(now_time)
174
+ end
175
+
176
+ it "parses floats as seconds past the epoch" do
177
+ user.created_at = now_time.to_f
178
+ # Going via float loses precision, so use be_within
179
+ expect(user.created_at).to be_within(0.0001).of(now_time)
180
+ expect(user.created_at).to be_a_kind_of(Time)
181
+ end
182
+
183
+ it "parses integers as milliseconds past the epoch" do
184
+ user.created_at = (now_time.to_f * 1000).to_i
185
+ # Truncating to milliseconds loses precision, so use be_within
186
+ expect(user.created_at).to be_within(0.001).of(now_time)
187
+ expect(user.created_at).to be_a_kind_of(Time)
188
+ end
189
+
190
+ it "parses strings to date/times" do
191
+ user.created_at = "2014-12-25 14:00:00 +0100"
192
+ expect(user.created_at).to eq(Time.new(2014, 12, 25, 13, 00, 00))
193
+ end
194
+
195
+ it "raises for unparseable strings" do
196
+ expect { user.created_at = "Today, innit?" }.to raise_error
197
+ end
198
+
199
+ it "converts Dates to Time" do
200
+ user.created_at = Date.parse("2014-12-25")
201
+ expect(user.created_at).to eq(Time.new(2014, 12, 25, 00, 00, 00))
202
+ end
203
+
204
+ it "converts DateTime to Time" do
205
+ user.created_at = DateTime.parse("2014-12-25 13:00:45")
206
+ expect(user.created_at).to eq(Time.new(2014, 12, 25, 13, 00, 45))
207
+ end
208
+
209
+ it "stores nil" do
210
+ user.created_at = now_time
211
+ user.created_at = nil
212
+ expect(user.created_at).to be_nil
213
+ end
214
+
215
+ it "does not provide a created_at? method" do
216
+ expect(user).to_not respond_to(:created_at?)
217
+ expect { user.created_at? }.to raise_error(NoMethodError)
218
+ end
219
+ end
220
+
221
+ describe "a json attribute (profile)" do
222
+ it "is nil when unset" do
223
+ expect(user.profile).to be_nil
224
+ end
225
+
226
+ it "stores a string" do
227
+ user.profile = 'Incomplete'
228
+ expect(user.profile).to eq('Incomplete')
229
+ end
230
+
231
+ it "stores an integer" do
232
+ user.profile = 3
233
+ expect(user.profile).to eq(3)
234
+ end
235
+
236
+ it "stores true" do
237
+ user.profile = true
238
+ expect(user.profile).to eq(true)
239
+ end
240
+
241
+ it "stores false" do
242
+ user.profile = false
243
+ expect(user.profile).to eq(false)
244
+ end
245
+
246
+ it "stores an array" do
247
+ user.profile = [1, 2, 3]
248
+ expect(user.profile).to eq([1, 2, 3])
249
+ end
250
+
251
+ it "stores a hash" do
252
+ user.profile = {'skill' => 8}
253
+ expect(user.profile).to eq({'skill' => 8})
254
+ end
255
+
256
+ it "stores nested hashes and arrays" do
257
+ json = {'array' => [1,
258
+ 2,
259
+ true,
260
+ {'inner' => true},
261
+ ['inside', {}]
262
+ ],
263
+ 'hash' => {'getting' => {'nested' => 'yes'}},
264
+ 'boolean' => true
265
+ }
266
+ user.profile = json
267
+ expect(user.profile).to eq(json)
268
+ end
269
+
270
+ it "raises when passed an object not supported by JSON" do
271
+ expect { user.profile = Object.new }.to raise_error
272
+ end
273
+
274
+ it "raises when passed a hash with a non-string key" do
275
+ expect { user.profile = {1 => 'first'} }.to raise_error
276
+ end
277
+
278
+ it "raises when passed a hash with an unsupported value" do
279
+ expect { user.profile = {'first' => :symbol} }.to raise_error
280
+ end
281
+
282
+ it "raises when passed an array with an unsupported value" do
283
+ expect { user.profile = [1, 2, nil, :symbol] }.to raise_error
284
+ end
285
+
286
+ it "stores nil" do
287
+ user.profile = {'foo' => 'bar'}
288
+ user.profile = nil
289
+ expect(user.profile).to be_nil
290
+ end
291
+
292
+ it "does not provide a profile? method" do
293
+ expect(user).to_not respond_to(:profile?)
294
+ expect { user.profile? }.to raise_error(NoMethodError)
295
+ end
296
+ end
297
+
298
+ describe "#write_attribute" do
299
+ it "does the same casting as using the writer method" do
300
+ user.write_attribute(:id, '3')
301
+ expect(user.id).to eq(3)
302
+ end
303
+
304
+ it "raises an error if passed an invalid attribute name" do
305
+ expect do
306
+ user.write_attribute(:spelling_mistake, '3')
307
+ end.to raise_error(ModelAttribute::InvalidAttributeNameError,
308
+ "Invalid attribute name :spelling_mistake")
309
+ end
310
+ end
311
+
312
+ describe "#read_attribute" do
313
+ it "returns the value of an attribute that has been set" do
314
+ user.write_attribute(:id, 3)
315
+ expect(user.read_attribute(:id)).to eq(user.id)
316
+ end
317
+
318
+ it "returns nil for an attribute that has not been set" do
319
+ expect(user.read_attribute(:id)).to be_nil
320
+ end
321
+
322
+ it "raises an error if passed an invalid attribute name" do
323
+ expect do
324
+ user.read_attribute(:spelling_mistake)
325
+ end.to raise_error(ModelAttribute::InvalidAttributeNameError,
326
+ "Invalid attribute name :spelling_mistake")
327
+ end
328
+ end
329
+
330
+ describe "#changes" do
331
+ let(:changes) { user.changes }
332
+
333
+ context "for a model instance created with no attributes" do
334
+ it "is empty" do
335
+ expect(changes).to be_empty
336
+ end
337
+ end
338
+
339
+ context "when an attribute is set via a writer method" do
340
+ before(:each) { user.id = 3 }
341
+
342
+ it "has an entry from attribute name to [old, new] pair" do
343
+ expect(changes).to include(:id => [nil, 3])
344
+ end
345
+
346
+ context "when an attribute is set again" do
347
+ before(:each) { user.id = 5 }
348
+
349
+ it "shows the latest value for the attribute" do
350
+ expect(changes).to include(:id => [nil, 5])
351
+ end
352
+ end
353
+
354
+ context "when an attribute is set back to its original value" do
355
+ before(:each) { user.id = nil }
356
+
357
+ it "does not have an entry for the attribute" do
358
+ expect(changes).to_not include(:id)
359
+ end
360
+ end
361
+ end
362
+ end
363
+
364
+ describe "#changes_for_json" do
365
+ let(:changes_for_json) { user.changes_for_json }
366
+
367
+ context "for a model instance created with no attributes" do
368
+ it "is empty" do
369
+ expect(changes_for_json).to be_empty
370
+ end
371
+ end
372
+
373
+ context "when an attribute is set via a writer method" do
374
+ before(:each) { user.id = 3 }
375
+
376
+ it "has an entry from attribute name (as a string) to the new value" do
377
+ expect(changes_for_json).to include('id' => 3)
378
+ end
379
+
380
+ context "when an attribute is set again" do
381
+ before(:each) { user.id = 5 }
382
+
383
+ it "shows the latest value for the attribute" do
384
+ expect(changes_for_json).to include('id' => 5)
385
+ end
386
+ end
387
+
388
+ context "when an attribute is set back to its original value" do
389
+ before(:each) { user.id = nil }
390
+
391
+ it "does not have an entry for the attribute" do
392
+ expect(changes_for_json).to_not include('id')
393
+ end
394
+ end
395
+
396
+ context "if the returned hash is modified" do
397
+ before(:each) { user.changes_for_json.clear }
398
+
399
+ it "does not affect subsequent results from changes_for_json" do
400
+ expect(changes_for_json).to include('id' => 3)
401
+ end
402
+ end
403
+ end
404
+
405
+ it "serializes time attributes as JSON integer" do
406
+ user.created_at = Time.now
407
+ expect(changes_for_json).to include("created_at" => instance_of(Fixnum))
408
+ end
409
+ end
410
+
411
+ describe "id_changed?" do
412
+ context "with no changes" do
413
+ it "returns false" do
414
+ expect(user.id_changed?).to eq(false)
415
+ end
416
+ end
417
+
418
+ context "with changes" do
419
+ before(:each) { user.id = 3 }
420
+
421
+ it "returns true" do
422
+ expect(user.id_changed?).to eq(true)
423
+ end
424
+ end
425
+ end
426
+
427
+ describe "#attributes" do
428
+ let(:time_now) { Time.now }
429
+
430
+ before(:each) do
431
+ user.id = 1
432
+ user.paid = true
433
+ user.created_at = time_now
434
+ end
435
+
436
+ it "returns a hash including each set attribute" do
437
+ expect(user.attributes).to include(id: 1, paid: true, created_at: time_now)
438
+ end
439
+
440
+ it "returns a hash with a nil value for each unset attribute" do
441
+ expect(user.attributes).to include(name: nil)
442
+ end
443
+ end
444
+
445
+ describe "#attributes_for_json" do
446
+ let(:time_now) { Time.now }
447
+
448
+ before(:each) do
449
+ user.id = 1
450
+ user.paid = true
451
+ user.created_at = time_now
452
+ end
453
+
454
+ it "serializes integer attributes as JSON integer" do
455
+ expect(user.attributes_for_json).to include("id" => 1)
456
+ end
457
+
458
+ it "serializes time attributes as JSON integer" do
459
+ expect(user.attributes_for_json).to include("created_at" => instance_of(Fixnum))
460
+ end
461
+
462
+ it "serializes string attributes as JSON string" do
463
+ user.name = 'Fred'
464
+ expect(user.attributes_for_json).to include("name" => "Fred")
465
+ end
466
+
467
+ it "leaves JSON attributes unchanged" do
468
+ json = {'interests' => ['coding', 'social networks'], 'rank' => 15}
469
+ user.profile = json
470
+ expect(user.attributes_for_json).to include("profile" => json)
471
+ end
472
+
473
+ it "omits attributes with a nil value" do
474
+ expect(user.attributes_for_json).to_not include("name")
475
+ end
476
+ end
477
+
478
+ describe "#set_attributes" do
479
+ it "allows mass assignment of attributes" do
480
+ user.set_attributes(id: 5, name: "Sally")
481
+ expect(user.attributes).to include(id: 5, name: "Sally")
482
+ end
483
+
484
+ it "ignores keys that have no writer method" do
485
+ user.set_attributes(id: 5, species: "Human")
486
+ expect(user.attributes).to_not include(species: "Human")
487
+ end
488
+
489
+ context "for an attribute with a private writer method" do
490
+ before(:all) { User.send(:private, :name=) }
491
+ after(:all) { User.send(:public, :name=) }
492
+
493
+ it "does not set the attribute" do
494
+ user.set_attributes(id: 5, name: "Sally")
495
+ expect(user.attributes).to_not include(name: "Sally")
496
+ end
497
+
498
+ it "sets the attribute if the flag is passed" do
499
+ user.set_attributes({id: 5, name: "Sally"}, true)
500
+ expect(user.attributes).to include(name: "Sally")
501
+ end
502
+ end
503
+ end
504
+
505
+ describe "#inspect" do
506
+ let(:user) do
507
+ User.new(id: 1,
508
+ name: "Fred",
509
+ created_at: "2014-12-25 08:00",
510
+ paid: true,
511
+ profile: {'interests' => ['coding', 'social networks'], 'rank' => 15})
512
+ end
513
+
514
+ it "includes integer attributes as 'name: value'" do
515
+ expect(user.inspect).to include("id: 1")
516
+ end
517
+
518
+ it "includes boolean attributes as 'name: true/false'" do
519
+ expect(user.inspect).to include("paid: true")
520
+ end
521
+
522
+ it "includes string attributes as 'name: \"string\"'" do
523
+ expect(user.inspect).to include('name: "Fred"')
524
+ end
525
+
526
+ it "includes time attributes as 'name: <ISO 8601>'" do
527
+ expect(user.inspect).to include("created_at: 2014-12-25 08:00:00 +0000")
528
+ end
529
+
530
+ it "includes json attributes as 'name: inspected_json'" do
531
+ expect(user.inspect).to include('profile: {"interests"=>["coding", "social networks"], "rank"=>15}')
532
+ end
533
+
534
+ it "includes the class name" do
535
+ expect(user.inspect).to include("User")
536
+ end
537
+
538
+ it "looks like '#<User id: 1, paid: true, name: ..., created_at: ...>'" do
539
+ expect(user.inspect).to eq("#<User id: 1, paid: true, name: \"Fred\", created_at: 2014-12-25 08:00:00 +0000, profile: {\"interests\"=>[\"coding\", \"social networks\"], \"rank\"=>15}>")
540
+ end
541
+ end
542
+
543
+ describe 'equality with :id field' do
544
+ let(:u1) { User.new(id: 1, name: 'David') }
545
+
546
+ context '#==' do
547
+ it 'returns true when ids match, regardless of other attributes' do
548
+ u2 = User.new(id: 1, name: 'Dave')
549
+ expect(u1).to eq(u2)
550
+ end
551
+
552
+ it 'returns false when ids do not match' do
553
+ u2 = User.new(id: 2, name: 'David')
554
+ expect(u1).to_not eq(u2)
555
+ end
556
+ end
557
+
558
+ context '#eql?' do
559
+ it 'returns true when ids match, regardless of other attributes' do
560
+ u2 = User.new(id: 1, name: 'Dave')
561
+ expect(u1).to eql(u2)
562
+ end
563
+
564
+ it 'returns false when ids do not match' do
565
+ u2 = User.new(id: 2, name: 'David')
566
+ expect(u1).to_not eql(u2)
567
+ end
568
+ end
569
+ end
570
+
571
+ describe 'equality without :id field' do
572
+ let(:u1) { UserWithoutId.new(name: 'David') }
573
+
574
+ context "for models with different attribute values" do
575
+ let(:u2) { UserWithoutId.new(name: 'Dave') }
576
+
577
+ it "#== returns false" do
578
+ expect(u1).to_not eq(u2)
579
+ end
580
+
581
+ it "#eql? returns false" do
582
+ expect(u1).to_not eql(u2)
583
+ end
584
+ end
585
+
586
+ context "for models with different attributes set" do
587
+ let(:u2) { UserWithoutId.new }
588
+
589
+ it "#== returns false" do
590
+ expect(u1).to_not eq(u2)
591
+ end
592
+
593
+ it "#eql? returns false" do
594
+ expect(u1).to_not eql(u2)
595
+ end
596
+ end
597
+
598
+ context "for models with the same attributes set to the same values" do
599
+ let(:u2) { UserWithoutId.new(name: 'David') }
600
+
601
+ it "#== returns true" do
602
+ expect(u1).to eq(u2)
603
+ end
604
+
605
+ it "#eql? returns true" do
606
+ expect(u1).to eql(u2)
607
+ end
608
+ end
609
+ end
610
+ end
611
+ end