taxjar-model_attribute 3.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,76 @@
1
+ module ModelAttribute
2
+ module Casts
3
+ class << self
4
+ def cast(value, type)
5
+ return nil if value.nil?
6
+
7
+ case type
8
+ when :integer
9
+ int = Integer(value)
10
+ float = Float(value)
11
+ raise ArgumentError, "Can't cast #{value.inspect} to an integer without loss of precision" unless int == float
12
+ int
13
+ when :float
14
+ Float(value)
15
+ when :boolean
16
+ if !!value == value
17
+ value
18
+ elsif value == 't'
19
+ true
20
+ elsif value == 'f'
21
+ false
22
+ else
23
+ raise ArgumentError, "Can't cast #{value.inspect} to boolean"
24
+ end
25
+ when :time
26
+ case value
27
+ when Time
28
+ value
29
+ when Date, DateTime
30
+ value.to_time
31
+ when Integer
32
+ # Assume milliseconds since epoch.
33
+ Time.at(value / 1000.0)
34
+ when Numeric
35
+ # Numeric, but not an integer. Assume seconds since epoch.
36
+ Time.at(value)
37
+ else
38
+ Time.parse(value)
39
+ end
40
+ when :string
41
+ String(value)
42
+ when :json
43
+ if valid_json?(value)
44
+ value
45
+ else
46
+ raise ArgumentError, "JSON only supports nil, numeric, string, boolean and arrays and hashes of those."
47
+ end
48
+ else
49
+ raise UnsupportedTypeError.new(type)
50
+ end
51
+ end
52
+
53
+ private
54
+
55
+ def valid_json?(value)
56
+ (value == nil ||
57
+ value == true ||
58
+ value == false ||
59
+ value.is_a?(Numeric) ||
60
+ value.is_a?(String) ||
61
+ (value.is_a?(Array) && valid_json_array?(value)) ||
62
+ (value.is_a?(Hash) && valid_json_hash?(value) ))
63
+ end
64
+
65
+ def valid_json_array?(array)
66
+ array.all? { |value| valid_json?(value) }
67
+ end
68
+
69
+ def valid_json_hash?(hash)
70
+ hash.all? do |key, value|
71
+ key.is_a?(String) && valid_json?(value)
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -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,3 @@
1
+ module ModelAttribute
2
+ VERSION = "3.1.0"
3
+ 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,669 @@
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
+ attribute :reward_points, :integer, default: 0
9
+ attribute :win_rate, :float
10
+
11
+ def initialize(attributes = {})
12
+ set_attributes(attributes)
13
+ end
14
+ end
15
+
16
+ class UserWithoutId
17
+ extend ModelAttribute
18
+ attribute :paid, :boolean
19
+ attribute :name, :string
20
+ attribute :created_at, :time
21
+
22
+ def initialize(attributes = {})
23
+ set_attributes(attributes)
24
+ end
25
+ end
26
+
27
+ RSpec.describe "a class using ModelAttribute" do
28
+ describe "class methods" do
29
+ describe ".attribute" do
30
+ context "passed an unrecognised type" do
31
+ it "raises an error" do
32
+ expect do
33
+ User.attribute :address, :custom_type
34
+ end.to raise_error(ModelAttribute::UnsupportedTypeError,
35
+ "Unsupported type :custom_type. " +
36
+ "Must be one of :integer, :float, :boolean, :string, :time, :json.")
37
+ end
38
+ end
39
+ end
40
+
41
+ describe ".attributes" do
42
+ it "returns an array of attribute names as symbols" do
43
+ expect(User.attributes).to eq([:id, :paid, :name, :created_at, :profile, :reward_points, :win_rate])
44
+ end
45
+ end
46
+
47
+ describe ".attribute_defaults" do
48
+ it "returns a hash of attributes that have non-nil defaults" do
49
+ expect(User.attribute_defaults).to eq({reward_points: 0})
50
+ end
51
+ end
52
+ end
53
+
54
+ describe "an instance of the class" do
55
+ let(:user) { User.new }
56
+
57
+ describe "an integer attribute (id)" do
58
+ it "is nil when unset" do
59
+ expect(user.id).to be_nil
60
+ end
61
+
62
+ it "stores an integer" do
63
+ user.id = 3
64
+ expect(user.id).to eq(3)
65
+ end
66
+
67
+ it "stores an integer passed as a float" do
68
+ user.id = 3.0
69
+ expect(user.id).to eq(3)
70
+ end
71
+
72
+ it "raises when passed a float with non-zero decimal part" do
73
+ expect { user.id = 3.3 }.to raise_error(ArgumentError)
74
+ end
75
+
76
+ it "parses an integer string" do
77
+ user.id = '3'
78
+ expect(user.id).to eq(3)
79
+ end
80
+
81
+ it "raises if passed a string it can't parse" do
82
+ expect { user.id = '3a' }.to raise_error(ArgumentError,
83
+ /invalid value for Integer.*: "3a"/)
84
+ end
85
+
86
+ it "stores nil" do
87
+ user.id = 3
88
+ user.id = nil
89
+ expect(user.id).to be_nil
90
+ end
91
+
92
+ it "does not provide an id? method" do
93
+ expect(user).to_not respond_to(:id?)
94
+ expect { user.id? }.to raise_error(NoMethodError)
95
+ end
96
+ end
97
+
98
+ describe "a float attribute (win_rate)" do
99
+ it "stores a float" do
100
+ user.win_rate = 35.62
101
+ expect(user.win_rate).to eq(35.62)
102
+ end
103
+
104
+ it "parses a float string" do
105
+ user.win_rate = 35.62
106
+ expect(user.win_rate).to eq(35.62)
107
+ end
108
+ end
109
+
110
+ describe "a boolean attribute (paid)" do
111
+ it "is nil when unset" do
112
+ expect(user.paid).to be_nil
113
+ end
114
+
115
+ it "stores true" do
116
+ user.paid = true
117
+ expect(user.paid).to eq(true)
118
+ end
119
+
120
+ it "stores false" do
121
+ user.paid = false
122
+ expect(user.paid).to eq(false)
123
+ end
124
+
125
+ it "parses 't' as true" do
126
+ user.paid = 't'
127
+ expect(user.paid).to eq(true)
128
+ end
129
+
130
+ it "parses 'f' as false" do
131
+ user.paid = 'f'
132
+ expect(user.paid).to eq(false)
133
+ end
134
+
135
+ it "raises if passed a string it can't parse" do
136
+ expect { user.paid = '3a' }.to raise_error(ArgumentError,
137
+ 'Can\'t cast "3a" to boolean')
138
+ end
139
+
140
+ it "stores nil" do
141
+ user.paid = true
142
+ user.paid = nil
143
+ expect(user.paid).to be_nil
144
+ end
145
+
146
+ describe "#paid?" do
147
+ it "returns false when unset" do
148
+ expect(user.paid?).to eq(false)
149
+ end
150
+
151
+ it "returns false for false attributes" do
152
+ user.paid = false
153
+ expect(user.paid?).to eq(false)
154
+ end
155
+
156
+ it "returns true for true attributes" do
157
+ user.paid = true
158
+ expect(user.paid?).to eq(true)
159
+ end
160
+ end
161
+ end
162
+
163
+ describe "a string attribute (name)" do
164
+ it "is nil when unset" do
165
+ expect(user.name).to be_nil
166
+ end
167
+
168
+ it "stores a string" do
169
+ user.name = 'Fred'
170
+ expect(user.name).to eq('Fred')
171
+ end
172
+
173
+ it "casts an integer to a string" do
174
+ user.name = 3
175
+ expect(user.name).to eq('3')
176
+ end
177
+
178
+ it "stores nil" do
179
+ user.name = 'Fred'
180
+ user.name = nil
181
+ expect(user.name).to be_nil
182
+ end
183
+
184
+ it "does not provide a name? method" do
185
+ expect(user).to_not respond_to(:name?)
186
+ expect { user.name? }.to raise_error(NoMethodError)
187
+ end
188
+ end
189
+
190
+ describe "a time attribute (created_at)" do
191
+ let(:now_time) { Time.now }
192
+
193
+ it "is nil when unset" do
194
+ expect(user.created_at).to be_nil
195
+ end
196
+
197
+ it "stores a Time object" do
198
+ user.created_at = now_time
199
+ expect(user.created_at).to eq(now_time)
200
+ end
201
+
202
+ it "parses floats as seconds past the epoch" do
203
+ user.created_at = now_time.to_f
204
+ # Going via float loses precision, so use be_within
205
+ expect(user.created_at).to be_within(0.0001).of(now_time)
206
+ expect(user.created_at).to be_a_kind_of(Time)
207
+ end
208
+
209
+ it "parses integers as milliseconds past the epoch" do
210
+ user.created_at = (now_time.to_f * 1000).to_i
211
+ # Truncating to milliseconds loses precision, so use be_within
212
+ expect(user.created_at).to be_within(0.001).of(now_time)
213
+ expect(user.created_at).to be_a_kind_of(Time)
214
+ end
215
+
216
+ it "parses strings to date/times" do
217
+ user.created_at = "2014-12-25 14:00:00 +0100"
218
+ expect(user.created_at).to eq(Time.new(2014, 12, 25, 13, 00, 00))
219
+ end
220
+
221
+ it "raises for unparseable strings" do
222
+ expect { user.created_at = "Today, innit?" }.to raise_error(ArgumentError,
223
+ 'no time information in "Today, innit?"')
224
+ end
225
+
226
+ it "converts Dates to Time" do
227
+ user.created_at = Date.parse("2014-12-25")
228
+ expect(user.created_at).to eq(Time.new(2014, 12, 25, 00, 00, 00))
229
+ end
230
+
231
+ it "converts DateTime to Time" do
232
+ user.created_at = DateTime.parse("2014-12-25 13:00:45")
233
+ expect(user.created_at).to eq(Time.new(2014, 12, 25, 13, 00, 45))
234
+ end
235
+
236
+ it "stores nil" do
237
+ user.created_at = now_time
238
+ user.created_at = nil
239
+ expect(user.created_at).to be_nil
240
+ end
241
+
242
+ it "does not provide a created_at? method" do
243
+ expect(user).to_not respond_to(:created_at?)
244
+ expect { user.created_at? }.to raise_error(NoMethodError)
245
+ end
246
+ end
247
+
248
+ describe "a json attribute (profile)" do
249
+ it "is nil when unset" do
250
+ expect(user.profile).to be_nil
251
+ end
252
+
253
+ it "stores a string" do
254
+ user.profile = 'Incomplete'
255
+ expect(user.profile).to eq('Incomplete')
256
+ end
257
+
258
+ it "stores an integer" do
259
+ user.profile = 3
260
+ expect(user.profile).to eq(3)
261
+ end
262
+
263
+ it "stores true" do
264
+ user.profile = true
265
+ expect(user.profile).to eq(true)
266
+ end
267
+
268
+ it "stores false" do
269
+ user.profile = false
270
+ expect(user.profile).to eq(false)
271
+ end
272
+
273
+ it "stores an array" do
274
+ user.profile = [1, 2, 3]
275
+ expect(user.profile).to eq([1, 2, 3])
276
+ end
277
+
278
+ it "stores a hash" do
279
+ user.profile = {'skill' => 8}
280
+ expect(user.profile).to eq({'skill' => 8})
281
+ end
282
+
283
+ it "stores nested hashes and arrays" do
284
+ json = {'array' => [1,
285
+ 2,
286
+ true,
287
+ {'inner' => true},
288
+ ['inside', {}]
289
+ ],
290
+ 'hash' => {'getting' => {'nested' => 'yes'}},
291
+ 'boolean' => true
292
+ }
293
+ user.profile = json
294
+ expect(user.profile).to eq(json)
295
+ end
296
+
297
+ it "raises when passed an object not supported by JSON" do
298
+ expect { user.profile = Object.new }.to raise_error(ArgumentError,
299
+ "JSON only supports nil, numeric, string, boolean and arrays and hashes of those.")
300
+ end
301
+
302
+ it "raises when passed a hash with a non-string key" do
303
+ expect { user.profile = {1 => 'first'} }.to raise_error(ArgumentError,
304
+ "JSON only supports nil, numeric, string, boolean and arrays and hashes of those.")
305
+ end
306
+
307
+ it "raises when passed a hash with an unsupported value" do
308
+ expect { user.profile = {'first' => :symbol} }.to raise_error(ArgumentError,
309
+ "JSON only supports nil, numeric, string, boolean and arrays and hashes of those.")
310
+ end
311
+
312
+ it "raises when passed an array with an unsupported value" do
313
+ expect { user.profile = [1, 2, nil, :symbol] }.to raise_error(ArgumentError,
314
+ "JSON only supports nil, numeric, string, boolean and arrays and hashes of those.")
315
+ end
316
+
317
+ it "stores nil" do
318
+ user.profile = {'foo' => 'bar'}
319
+ user.profile = nil
320
+ expect(user.profile).to be_nil
321
+ end
322
+
323
+ it "does not provide a profile? method" do
324
+ expect(user).to_not respond_to(:profile?)
325
+ expect { user.profile? }.to raise_error(NoMethodError)
326
+ end
327
+ end
328
+
329
+ describe 'a defaulted attribute (reward_points)' do
330
+ it "returns the default when unset" do
331
+ expect(user.reward_points).to eq(0)
332
+ end
333
+ end
334
+
335
+ describe "#write_attribute" do
336
+ it "does the same casting as using the writer method" do
337
+ user.write_attribute(:id, '3')
338
+ expect(user.id).to eq(3)
339
+ end
340
+
341
+ it "raises an error if passed an invalid attribute name" do
342
+ expect do
343
+ user.write_attribute(:spelling_mistake, '3')
344
+ end.to raise_error(ModelAttribute::InvalidAttributeNameError,
345
+ "Invalid attribute name :spelling_mistake")
346
+ end
347
+ end
348
+
349
+ describe "#read_attribute" do
350
+ it "returns the value of an attribute that has been set" do
351
+ user.write_attribute(:id, 3)
352
+ expect(user.read_attribute(:id)).to eq(user.id)
353
+ end
354
+
355
+ it "returns nil for an attribute that has not been set" do
356
+ expect(user.read_attribute(:id)).to be_nil
357
+ end
358
+
359
+ context "for an attribute with a default" do
360
+ it "returns the default if the attribute has not been set" do
361
+ expect(user.read_attribute(:reward_points)).to eq(0)
362
+ end
363
+ end
364
+
365
+ it "raises an error if passed an invalid attribute name" do
366
+ expect do
367
+ user.read_attribute(:spelling_mistake)
368
+ end.to raise_error(ModelAttribute::InvalidAttributeNameError,
369
+ "Invalid attribute name :spelling_mistake")
370
+ end
371
+ end
372
+
373
+ describe "#changes" do
374
+ let(:changes) { user.changes }
375
+
376
+ context "for a model instance created with no attributes except defaults" do
377
+ it "is empty" do
378
+ expect(changes).to be_empty
379
+ end
380
+ end
381
+
382
+ context "when an attribute is set via a writer method" do
383
+ before(:each) { user.id = 3 }
384
+
385
+ it "has an entry from attribute name to [old, new] pair" do
386
+ expect(changes).to include(:id => [nil, 3])
387
+ end
388
+
389
+ context "when an attribute is set again" do
390
+ before(:each) { user.id = 5 }
391
+
392
+ it "shows the latest value for the attribute" do
393
+ expect(changes).to include(:id => [nil, 5])
394
+ end
395
+ end
396
+
397
+ context "when an attribute is set back to its original value" do
398
+ before(:each) { user.id = nil }
399
+
400
+ it "does not have an entry for the attribute" do
401
+ expect(changes).to_not include(:id)
402
+ end
403
+ end
404
+ end
405
+ end
406
+
407
+ describe "#changes_for_json" do
408
+ let(:changes_for_json) { user.changes_for_json }
409
+
410
+ context "for a model instance created with no attributes" do
411
+ it "is empty" do
412
+ expect(changes_for_json).to be_empty
413
+ end
414
+ end
415
+
416
+ context "when an attribute is set via a writer method" do
417
+ before(:each) { user.id = 3 }
418
+
419
+ it "has an entry from attribute name (as a string) to the new value" do
420
+ expect(changes_for_json).to include('id' => 3)
421
+ end
422
+
423
+ context "when an attribute is set again" do
424
+ before(:each) { user.id = 5 }
425
+
426
+ it "shows the latest value for the attribute" do
427
+ expect(changes_for_json).to include('id' => 5)
428
+ end
429
+ end
430
+
431
+ context "when an attribute is set back to its original value" do
432
+ before(:each) { user.id = nil }
433
+
434
+ it "does not have an entry for the attribute" do
435
+ expect(changes_for_json).to_not include('id')
436
+ end
437
+ end
438
+
439
+ context "if the returned hash is modified" do
440
+ before(:each) { user.changes_for_json.clear }
441
+
442
+ it "does not affect subsequent results from changes_for_json" do
443
+ expect(changes_for_json).to include('id' => 3)
444
+ end
445
+ end
446
+ end
447
+
448
+ it "serializes time attributes as JSON integer" do
449
+ user.created_at = Time.now
450
+ expect(changes_for_json).to include("created_at" => instance_of(Fixnum))
451
+ end
452
+ end
453
+
454
+ describe "#id_changed?" do
455
+ context "with no changes" do
456
+ it "returns false" do
457
+ expect(user.id_changed?).to eq(false)
458
+ end
459
+ end
460
+
461
+ context "with changes" do
462
+ before(:each) { user.id = 3 }
463
+
464
+ it "returns true" do
465
+ expect(user.id_changed?).to eq(true)
466
+ end
467
+ end
468
+ end
469
+
470
+ describe "#attributes" do
471
+ let(:time_now) { Time.now }
472
+
473
+ before(:each) do
474
+ user.id = 1
475
+ user.paid = true
476
+ user.created_at = time_now
477
+ end
478
+
479
+ it "returns a hash including each set attribute" do
480
+ expect(user.attributes).to include(id: 1, paid: true, created_at: time_now)
481
+ end
482
+
483
+ it "returns a hash with a nil value for each unset attribute" do
484
+ expect(user.attributes).to include(name: nil)
485
+ end
486
+ end
487
+
488
+ describe "#attributes_for_json" do
489
+ let(:time_now) { Time.now }
490
+
491
+ before(:each) do
492
+ user.id = 1
493
+ user.paid = true
494
+ user.created_at = time_now
495
+ end
496
+
497
+ it "serializes integer attributes as JSON integer" do
498
+ expect(user.attributes_for_json).to include("id" => 1)
499
+ end
500
+
501
+ it "serializes time attributes as JSON integer" do
502
+ expect(user.attributes_for_json).to include("created_at" => instance_of(Fixnum))
503
+ end
504
+
505
+ it "serializes string attributes as JSON string" do
506
+ user.name = 'Fred'
507
+ expect(user.attributes_for_json).to include("name" => "Fred")
508
+ end
509
+
510
+ it "leaves JSON attributes unchanged" do
511
+ json = {'interests' => ['coding', 'social networks'], 'rank' => 15}
512
+ user.profile = json
513
+ expect(user.attributes_for_json).to include("profile" => json)
514
+ end
515
+
516
+ it "omits attributes still set to the default value" do
517
+ expect(user.attributes_for_json).to_not include("name", "reward_points")
518
+ end
519
+
520
+ it "includes an attribute changed from its default value" do
521
+ user.name = "Fred"
522
+ expect(user.attributes_for_json).to include("name" => "Fred")
523
+ end
524
+
525
+ it "includes an attribute changed from its default value to nil" do
526
+ user.reward_points = nil
527
+ expect(user.attributes_for_json).to include("reward_points" => nil)
528
+ end
529
+ end
530
+
531
+ describe "#set_attributes" do
532
+ it "allows mass assignment of attributes" do
533
+ user.set_attributes(id: 5, name: "Sally")
534
+ expect(user.attributes).to include(id: 5, name: "Sally")
535
+ end
536
+
537
+ it "ignores keys that have no writer method" do
538
+ user.set_attributes(id: 5, species: "Human")
539
+ expect(user.attributes).to_not include(species: "Human")
540
+ end
541
+
542
+ context "for an attribute with a private writer method" do
543
+ before(:all) { User.send(:private, :name=) }
544
+ after(:all) { User.send(:public, :name=) }
545
+
546
+ it "does not set the attribute" do
547
+ user.set_attributes(id: 5, name: "Sally")
548
+ expect(user.attributes).to_not include(name: "Sally")
549
+ end
550
+
551
+ it "sets the attribute if the flag is passed" do
552
+ user.set_attributes({id: 5, name: "Sally"}, true)
553
+ expect(user.attributes).to include(name: "Sally")
554
+ end
555
+ end
556
+ end
557
+
558
+ describe "#inspect" do
559
+ let(:user) do
560
+ User.new(id: 1,
561
+ name: "Fred",
562
+ created_at: "2014-12-25 08:00",
563
+ paid: true,
564
+ profile: {'interests' => ['coding', 'social networks'], 'rank' => 15},
565
+ win_rate: 35.62)
566
+ end
567
+
568
+ it "includes integer attributes as 'name: value'" do
569
+ expect(user.inspect).to include("id: 1")
570
+ end
571
+
572
+ it "includes boolean attributes as 'name: true/false'" do
573
+ expect(user.inspect).to include("paid: true")
574
+ end
575
+
576
+ it "includes string attributes as 'name: \"string\"'" do
577
+ expect(user.inspect).to include('name: "Fred"')
578
+ end
579
+
580
+ it "includes time attributes as 'name: <ISO 8601>'" do
581
+ expect(user.inspect).to include("created_at: 2014-12-25 08:00:00 +0000")
582
+ end
583
+
584
+ it "includes json attributes as 'name: inspected_json'" do
585
+ expect(user.inspect).to include('profile: {"interests"=>["coding", "social networks"], "rank"=>15}')
586
+ end
587
+
588
+ it "includes defaulted attributes" do
589
+ expect(user.inspect).to include('reward_points: 0')
590
+ end
591
+
592
+ it "includes the class name" do
593
+ expect(user.inspect).to include("User")
594
+ end
595
+
596
+ it "looks like '#<User id: 1, paid: true, name: ..., created_at: ...>'" do
597
+ 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}, reward_points: 0, win_rate: 35.62>")
598
+ end
599
+ end
600
+
601
+ describe 'equality with :id field' do
602
+ let(:u1) { User.new(id: 1, name: 'David') }
603
+
604
+ context '#==' do
605
+ it 'returns true when ids match, regardless of other attributes' do
606
+ u2 = User.new(id: 1, name: 'Dave')
607
+ expect(u1).to eq(u2)
608
+ end
609
+
610
+ it 'returns false when ids do not match' do
611
+ u2 = User.new(id: 2, name: 'David')
612
+ expect(u1).to_not eq(u2)
613
+ end
614
+ end
615
+
616
+ context '#eql?' do
617
+ it 'returns true when ids match, regardless of other attributes' do
618
+ u2 = User.new(id: 1, name: 'Dave')
619
+ expect(u1).to eql(u2)
620
+ end
621
+
622
+ it 'returns false when ids do not match' do
623
+ u2 = User.new(id: 2, name: 'David')
624
+ expect(u1).to_not eql(u2)
625
+ end
626
+ end
627
+ end
628
+
629
+ describe 'equality without :id field' do
630
+ let(:u1) { UserWithoutId.new(name: 'David') }
631
+
632
+ context "for models with different attribute values" do
633
+ let(:u2) { UserWithoutId.new(name: 'Dave') }
634
+
635
+ it "#== returns false" do
636
+ expect(u1).to_not eq(u2)
637
+ end
638
+
639
+ it "#eql? returns false" do
640
+ expect(u1).to_not eql(u2)
641
+ end
642
+ end
643
+
644
+ context "for models with different attributes set" do
645
+ let(:u2) { UserWithoutId.new }
646
+
647
+ it "#== returns false" do
648
+ expect(u1).to_not eq(u2)
649
+ end
650
+
651
+ it "#eql? returns false" do
652
+ expect(u1).to_not eql(u2)
653
+ end
654
+ end
655
+
656
+ context "for models with the same attributes set to the same values" do
657
+ let(:u2) { UserWithoutId.new(name: 'David') }
658
+
659
+ it "#== returns true" do
660
+ expect(u1).to eq(u2)
661
+ end
662
+
663
+ it "#eql? returns true" do
664
+ expect(u1).to eql(u2)
665
+ end
666
+ end
667
+ end
668
+ end
669
+ end