model_attribute 0.0.1 → 2.0.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.
@@ -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