taxjar-model_attribute 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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