model_attribute 0.0.1 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|