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.
- checksums.yaml +8 -8
- data/.rspec +2 -0
- data/CHANGELOG.md +34 -0
- data/Gemfile +1 -0
- data/Guardfile +47 -0
- data/LICENSE.txt +5 -1
- data/README.md +165 -5
- data/Rakefile +0 -1
- data/lib/model_attribute.rb +198 -1
- data/lib/model_attribute/errors.rb +14 -0
- data/lib/model_attribute/json.rb +27 -0
- data/lib/model_attribute/version.rb +1 -1
- data/model_attribute.gemspec +8 -3
- data/performance_comparison.rb +66 -0
- data/spec/model_attributes_spec.rb +611 -0
- data/spec/spec_helper.rb +92 -0
- metadata +84 -4
@@ -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
|
data/model_attribute.gemspec
CHANGED
@@ -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{
|
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",
|
21
|
-
spec.add_development_dependency "rake",
|
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
|