fast_attributes_rails 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,423 @@
1
+ require 'spec_helper'
2
+
3
+ describe FastAttributes do
4
+ describe '.type_casting' do
5
+ it 'returns predefined type casting rules' do
6
+ expect(FastAttributes.type_casting.keys).to include(String)
7
+ expect(FastAttributes.type_casting.keys).to include(Integer)
8
+ expect(FastAttributes.type_casting.keys).to include(Float)
9
+ expect(FastAttributes.type_casting.keys).to include(Array)
10
+ expect(FastAttributes.type_casting.keys).to include(Date)
11
+ expect(FastAttributes.type_casting.keys).to include(Time)
12
+ expect(FastAttributes.type_casting.keys).to include(DateTime)
13
+ expect(FastAttributes.type_casting.keys).to include(BigDecimal)
14
+ end
15
+ end
16
+
17
+ describe '.get_type_casting' do
18
+ it 'returns type casting function' do
19
+ expect(FastAttributes.get_type_casting(String)).to be_a(FastAttributes::TypeCast)
20
+ expect(FastAttributes.get_type_casting(Time)).to be_a(FastAttributes::TypeCast)
21
+ end
22
+ end
23
+
24
+ describe '.set_type_casting' do
25
+ after do
26
+ FastAttributes.remove_type_casting(OpenStruct)
27
+ end
28
+
29
+ it 'adds type to supported type casting list' do
30
+ expect(FastAttributes.get_type_casting(OpenStruct)).to be(nil)
31
+ FastAttributes.set_type_casting(OpenStruct, 'OpenStruct.new(a: %s)')
32
+ expect(FastAttributes.get_type_casting(OpenStruct)).to be_a(FastAttributes::TypeCast)
33
+ end
34
+ end
35
+
36
+ describe '.remove_type_casting' do
37
+ before do
38
+ FastAttributes.set_type_casting(OpenStruct, 'OpenStruct.new(a: %s)')
39
+ end
40
+
41
+ it 'removes type casting function from supported list' do
42
+ FastAttributes.remove_type_casting(OpenStruct)
43
+ expect(FastAttributes.get_type_casting(OpenStruct)).to be(nil)
44
+ end
45
+ end
46
+
47
+ describe '.type_exists?' do
48
+ it 'checks if type is registered' do
49
+ expect(FastAttributes.type_exists?(DateTime)).to be(true)
50
+ expect(FastAttributes.type_exists?(OpenStruct)).to be(false)
51
+ end
52
+ end
53
+
54
+ describe '#attribute' do
55
+ it 'raises an exception when type is not supported' do
56
+ type = Class.new(Object) { def self.inspect; 'CustomType' end }
57
+ klass = Class.new(Object) { extend FastAttributes }
58
+ expect{klass.attribute(:name, type)}.to raise_error(FastAttributes::UnsupportedTypeError, 'Unsupported attribute type "CustomType"')
59
+ expect{klass.attribute(:name, :type)}.to raise_error(FastAttributes::UnsupportedTypeError, 'Unsupported attribute type ":type"')
60
+ end
61
+
62
+ it 'generates getter methods' do
63
+ book = Book.new
64
+ expect(book.respond_to?(:title)).to be(true)
65
+ expect(book.respond_to?(:name)).to be(true)
66
+ expect(book.respond_to?(:pages)).to be(true)
67
+ expect(book.respond_to?(:price)).to be(true)
68
+ expect(book.respond_to?(:authors)).to be(true)
69
+ expect(book.respond_to?(:published)).to be(true)
70
+ expect(book.respond_to?(:sold)).to be(true)
71
+ expect(book.respond_to?(:finished)).to be(true)
72
+ expect(book.respond_to?(:rate)).to be(true)
73
+ end
74
+
75
+ it 'is possible to override getter method' do
76
+ toy = Toy.new
77
+ expect(toy.name).to eq(' toy!')
78
+ toy.name = 'bear'
79
+ expect(toy.name).to eq('bear toy!')
80
+ end
81
+
82
+ it 'generates setter methods' do
83
+ book = Book.new
84
+ expect(book.respond_to?(:title=)).to be(true)
85
+ expect(book.respond_to?(:name=)).to be(true)
86
+ expect(book.respond_to?(:pages=)).to be(true)
87
+ expect(book.respond_to?(:price=)).to be(true)
88
+ expect(book.respond_to?(:authors=)).to be(true)
89
+ expect(book.respond_to?(:published=)).to be(true)
90
+ expect(book.respond_to?(:sold=)).to be(true)
91
+ expect(book.respond_to?(:finished=)).to be(true)
92
+ expect(book.respond_to?(:rate=)).to be(true)
93
+ end
94
+
95
+ it 'is possible to override setter method' do
96
+ toy = Toy.new
97
+ expect(toy.price).to be(nil)
98
+ toy.price = 2
99
+ expect(toy.price).to eq(4)
100
+ end
101
+
102
+ it 'setter methods convert values to correct datatype' do
103
+ book = Book.new
104
+ book.title = 123
105
+ book.name = 456
106
+ book.pages = '250'
107
+ book.price = '2.55'
108
+ book.authors = 'Jobs'
109
+ book.published = '2014-06-21'
110
+ book.sold = '2014-06-21 20:45:15'
111
+ book.finished = '2014-05-20 21:35:20'
112
+ book.rate = '4.1'
113
+
114
+ expect(book.title).to eq('123')
115
+ expect(book.name).to eq('456')
116
+ expect(book.pages).to be(250)
117
+ expect(book.price).to eq(BigDecimal.new('2.55'))
118
+ expect(book.authors).to eq(%w[Jobs])
119
+ expect(book.published).to eq(Date.new(2014, 6, 21))
120
+ expect(book.sold).to eq(Time.new(2014, 6, 21, 20, 45, 15))
121
+ expect(book.finished).to eq(DateTime.new(2014, 5, 20, 21, 35, 20))
122
+ expect(book.rate).to eq(4.1)
123
+ end
124
+
125
+ it 'setter methods accept values which are already in a proper type' do
126
+ book = Book.new
127
+ book.title = title = 'One'
128
+ book.name = name = 'Two'
129
+ book.pages = pages = 250
130
+ book.price = price = BigDecimal.new('2.55')
131
+ book.authors = authors = %w[Jobs]
132
+ book.published = published = Date.new(2014, 06, 21)
133
+ book.sold = sold = Time.new(2014, 6, 21, 20, 45, 15)
134
+ book.finished = finished = DateTime.new(2014, 05, 20, 21, 35, 20)
135
+ book.rate = rate = 4.1
136
+
137
+ expect(book.title).to be(title)
138
+ expect(book.name).to be(name)
139
+ expect(book.pages).to be(pages)
140
+ expect(book.price).to eq(price)
141
+ expect(book.authors).to be(authors)
142
+ expect(book.published).to be(published)
143
+ expect(book.sold).to be(sold)
144
+ expect(book.finished).to be(finished)
145
+ expect(book.rate).to be(rate)
146
+ end
147
+
148
+ it 'setter methods accept nil values' do
149
+ book = Book.new
150
+ book.title = 'One'
151
+ book.name = 'Two'
152
+ book.pages = 250
153
+ book.price = BigDecimal.new('2.55')
154
+ book.authors = %w[Jobs]
155
+ book.published = Date.new(2014, 06, 21)
156
+ book.sold = Time.new(2014, 6, 21, 20, 45, 15)
157
+ book.finished = DateTime.new(2014, 05, 20, 21, 35, 20)
158
+ book.rate = 4.1
159
+
160
+ book.title = nil
161
+ book.name = nil
162
+ book.pages = nil
163
+ book.price = nil
164
+ book.authors = nil
165
+ book.published = nil
166
+ book.sold = nil
167
+ book.finished = nil
168
+ book.rate = nil
169
+
170
+ expect(book.title).to be(nil)
171
+ expect(book.name).to be(nil)
172
+ expect(book.pages).to be(nil)
173
+ expect(book.price).to be(nil)
174
+ expect(book.authors).to be(nil)
175
+ expect(book.published).to be(nil)
176
+ expect(book.sold).to be(nil)
177
+ expect(book.finished).to be(nil)
178
+ expect(book.rate).to be(nil)
179
+ end
180
+
181
+ it 'setter methods raise an exception when cannot parse values' do
182
+ object = BasicObject.new
183
+ def object.to_s; 'BasicObject'; end
184
+ def object.to_str; 1/0 end
185
+
186
+ book = Book.new
187
+ expect{ book.title = object }.to raise_error(FastAttributes::TypeCast::InvalidValueError, 'Invalid value "BasicObject" for attribute "title" of type "String"')
188
+ expect{ book.name = object }.to raise_error(FastAttributes::TypeCast::InvalidValueError, 'Invalid value "BasicObject" for attribute "name" of type "String"')
189
+ expect{ book.pages = 'number' }.to raise_error(FastAttributes::TypeCast::InvalidValueError, 'Invalid value "number" for attribute "pages" of type "Integer"')
190
+ expect{ book.price = 'bigdecimal' }.to raise_error(FastAttributes::TypeCast::InvalidValueError, 'Invalid value "bigdecimal" for attribute "price" of type "BigDecimal"')
191
+ expect{ book.published = 'date' }.to raise_error(FastAttributes::TypeCast::InvalidValueError, 'Invalid value "date" for attribute "published" of type "Date"')
192
+ expect{ book.sold = 'time' }.to raise_error(FastAttributes::TypeCast::InvalidValueError, 'Invalid value "time" for attribute "sold" of type "Time"')
193
+ expect{ book.finished = 'datetime' }.to raise_error(FastAttributes::TypeCast::InvalidValueError, 'Invalid value "datetime" for attribute "finished" of type "DateTime"')
194
+ expect{ book.rate = 'float' }.to raise_error(FastAttributes::TypeCast::InvalidValueError, 'Invalid value "float" for attribute "rate" of type "Float"')
195
+ end
196
+
197
+ it 'setter method can escape placeholder using double %' do
198
+ placeholder = PlaceholderClass.new
199
+ placeholder.value = 3
200
+ expect(placeholder.value).to eq('value %s %value %%s 2')
201
+ end
202
+
203
+ it 'setter method can accept %a placeholder which return attribute name' do
204
+ placeholder = PlaceholderClass.new
205
+
206
+ placeholder.title = 'attribute name 1'
207
+ expect(placeholder.title).to eq('title')
208
+
209
+ placeholder.title = 'attribute name 2'
210
+ expect(placeholder.title).to eq('title%a%title%title!')
211
+ end
212
+
213
+ it 'generates lenient attributes which do not correspond to a particular data type' do
214
+ lenient_attribute = LenientAttributes.new
215
+ expect(lenient_attribute.terms_of_service).to be(nil)
216
+
217
+ lenient_attribute.terms_of_service = 'yes'
218
+ expect(lenient_attribute.terms_of_service).to be(true)
219
+
220
+ lenient_attribute.terms_of_service = 'no'
221
+ expect(lenient_attribute.terms_of_service).to be(false)
222
+
223
+ lenient_attribute.terms_of_service = 42
224
+ expect(lenient_attribute.terms_of_service).to be(nil)
225
+ end
226
+
227
+ it 'allows to define attributes using symbols as a data type' do
228
+ book = DefaultLenientAttributes.new
229
+ book.title = title = 'One'
230
+ book.pages = pages = 250
231
+ book.price = price = BigDecimal.new('2.55')
232
+ book.authors = authors = %w[Jobs]
233
+ book.published = published = Date.new(2014, 06, 21)
234
+ book.sold = sold = Time.new(2014, 6, 21, 20, 45, 15)
235
+ book.finished = finished = DateTime.new(2014, 05, 20, 21, 35, 20)
236
+ book.rate = rate = 4.1
237
+
238
+ expect(book.title).to be(title)
239
+ expect(book.pages).to be(pages)
240
+ expect(book.price).to eq(price)
241
+ expect(book.authors).to be(authors)
242
+ expect(book.published).to be(published)
243
+ expect(book.sold).to be(sold)
244
+ expect(book.finished).to be(finished)
245
+ expect(book.rate).to be(rate)
246
+ end
247
+
248
+ context 'boolean attribute' do
249
+ let(:object) { DefaultLenientAttributes.new }
250
+
251
+ context 'when value is not set' do
252
+ it 'return nil' do
253
+ expect(object.active).to be(nil)
254
+ end
255
+ end
256
+
257
+ context 'when value represents true' do
258
+ it 'returns true' do
259
+ object.active = true
260
+ expect(object.active).to be(true)
261
+
262
+ object.active = 1
263
+ expect(object.active).to be(true)
264
+
265
+ object.active = '1'
266
+ expect(object.active).to be(true)
267
+
268
+ object.active = 't'
269
+ expect(object.active).to be(true)
270
+
271
+ object.active = 'T'
272
+ expect(object.active).to be(true)
273
+
274
+ object.active = 'true'
275
+ expect(object.active).to be(true)
276
+
277
+ object.active = 'TRUE'
278
+ expect(object.active).to be(true)
279
+
280
+ object.active = 'on'
281
+ expect(object.active).to be(true)
282
+
283
+ object.active = 'ON'
284
+ expect(object.active).to be(true)
285
+ end
286
+ end
287
+
288
+ context 'when value represents false' do
289
+ it 'returns false' do
290
+ object.active = false
291
+ expect(object.active).to be(false)
292
+
293
+ object.active = 0
294
+ expect(object.active).to be(false)
295
+
296
+ object.active = '0'
297
+ expect(object.active).to be(false)
298
+
299
+ object.active = 'f'
300
+ expect(object.active).to be(false)
301
+
302
+ object.active = 'F'
303
+ expect(object.active).to be(false)
304
+
305
+ object.active = 'false'
306
+ expect(object.active).to be(false)
307
+
308
+ object.active = 'FALSE'
309
+ expect(object.active).to be(false)
310
+
311
+ object.active = 'off'
312
+ expect(object.active).to be(false)
313
+
314
+ object.active = 'OFF'
315
+ expect(object.active).to be(false)
316
+ end
317
+ end
318
+ end
319
+ end
320
+
321
+ describe '#define_attributes' do
322
+ describe 'option initialize: true' do
323
+ it 'generates initialize method' do
324
+ reader = Reader.new(name: 104, age: '23')
325
+ expect(reader.name).to eq('104')
326
+ expect(reader.age).to be(23)
327
+ end
328
+
329
+ it 'is possible to override initialize method' do
330
+ window = Window.new
331
+ expect(window.height).to be(200)
332
+ expect(window.width).to be(80)
333
+
334
+ window = Window.new(height: 210, width: 100)
335
+ expect(window.height).to be(210)
336
+ expect(window.width).to be(100)
337
+ end
338
+ end
339
+
340
+ describe 'option attributes: true' do
341
+ it 'generates attributes method' do
342
+ publisher = Publisher.new
343
+ expect(publisher.attributes).to eq({'name' => nil, 'books' => nil})
344
+
345
+ reader = Reader.new
346
+ expect(reader.attributes).to eq({'name' => nil, 'age' => nil})
347
+ end
348
+
349
+ it 'is possible to override attributes method' do
350
+ window = Window.new(height: 220, width: 100)
351
+ expect(window.attributes).to eq({'height' => 220, 'width' => 100, 'color' => 'white'})
352
+ end
353
+
354
+ it 'attributes method return all attributes with their values' do
355
+ publisher = Publisher.new
356
+ publisher.name = 101
357
+ publisher.books = '20'
358
+ expect(publisher.attributes).to eq({'name' => '101', 'books' => 20})
359
+
360
+ reader = Reader.new
361
+ reader.name = 102
362
+ reader.age = '25'
363
+ expect(reader.attributes).to eq({'name' => '102', 'age' => 25})
364
+ end
365
+ end
366
+
367
+ describe 'option attributes: :accessors' do
368
+ it 'doesn\'t interfere when you don\'t use the option' do
369
+ klass = AttributesWithoutAccessors.new
370
+ expect(klass.attributes).to eq({'title' => nil, 'pages' => nil, 'color' => 'white'})
371
+ end
372
+
373
+ it "is returns the values of accessors, not the ivars" do
374
+ klass = AttributesWithAccessors.new(pages: 10, title: 'Something')
375
+ expect(klass.attributes['pages']).to be(20)
376
+ expect(klass.attributes['title']).to eq('A Longer Title: Something')
377
+ end
378
+
379
+ it 'is possible to override attributes method' do
380
+ klass = AttributesWithAccessors.new(pages: 10, title: 'Something')
381
+ expect(klass.attributes).to eq({'pages' => 20, 'title' => 'A Longer Title: Something', 'color' => 'white'})
382
+ end
383
+
384
+ it 'works with default attributes' do
385
+ klass = AttributesWithAccessorsAndDefaults.new
386
+ expect(klass.attributes).to eq({'pages' => 20, 'title' => 'a title'})
387
+ end
388
+ end
389
+ end
390
+
391
+ describe "default attributes" do
392
+ it "sets the default values" do
393
+ class_with_defaults = ClassWithDefaults.new
394
+
395
+ expect(class_with_defaults.title).to eq('a title')
396
+ expect(class_with_defaults.pages).to be(10)
397
+ expect(class_with_defaults.authors).to eq([1, 2, 4])
398
+ end
399
+
400
+ it "allows you to override default values" do
401
+ class_with_defaults = ClassWithDefaults.new(title: 'Something', authors: [1, 5, 7])
402
+
403
+ expect(class_with_defaults.title).to eq('Something')
404
+ expect(class_with_defaults.pages).to be(10)
405
+ expect(class_with_defaults.authors).to eq([1, 5, 7])
406
+ end
407
+
408
+ it "allows callable default values" do
409
+ class_with_defaults = ClassWithDefaults.new
410
+
411
+ expect(class_with_defaults.callable).to eq("callable value")
412
+ end
413
+
414
+ it "doesn't use the same instance between multiple instances" do
415
+ class_with_defaults = ClassWithDefaults.new
416
+ class_with_defaults.authors << 2
417
+
418
+ class_with_defaults2 = ClassWithDefaults.new
419
+
420
+ expect(class_with_defaults2.authors).to eq([1, 2, 4])
421
+ end
422
+ end
423
+ end
@@ -0,0 +1,177 @@
1
+ class Book
2
+ extend FastAttributes
3
+
4
+ attribute :title, :name, String
5
+ attribute :pages, Integer
6
+ attribute :price, BigDecimal
7
+ attribute :authors, Array
8
+ attribute :published, Date
9
+ attribute :sold, Time
10
+ attribute :finished, DateTime
11
+ attribute :rate, Float
12
+ end
13
+
14
+ class Author
15
+ extend FastAttributes
16
+
17
+ define_attributes initialize: true do
18
+ attribute :name, String
19
+ attribute :age, Integer
20
+ end
21
+ end
22
+
23
+ class Publisher
24
+ extend FastAttributes
25
+
26
+ define_attributes attributes: true do
27
+ attribute :name, String
28
+ attribute :books, Integer
29
+ end
30
+ end
31
+
32
+ class Reader
33
+ extend FastAttributes
34
+
35
+ define_attributes initialize: true, attributes: true do
36
+ attribute :name, String
37
+ attribute :age, Integer
38
+ end
39
+ end
40
+
41
+ class Toy
42
+ extend FastAttributes
43
+
44
+ attribute :name, String
45
+ attribute :price, Float
46
+
47
+ def name
48
+ "#{super} toy!"
49
+ end
50
+
51
+ def price=(value)
52
+ super((value.to_f + 2).to_s)
53
+ end
54
+ end
55
+
56
+ class Window
57
+ extend FastAttributes
58
+
59
+ define_attributes initialize: true, attributes: true do
60
+ attribute :height, Integer
61
+ attribute :width, Integer
62
+ end
63
+
64
+ def initialize(attributes = {})
65
+ self.height = 200
66
+ self.width = 80
67
+
68
+ super(attributes)
69
+ end
70
+
71
+ def attributes
72
+ super.merge('color' => 'white')
73
+ end
74
+ end
75
+
76
+ class Placeholder < String
77
+ end
78
+
79
+ FastAttributes.type_cast Placeholder do
80
+ from '"attribute name 1"', to: '"%a"'
81
+ from '"attribute name 2"', to: %q("%a%%a%%%a%#{%a<<'!'}")
82
+ otherwise '"%s %%s %%%s %%%%s #{5%%%s}"'
83
+ end
84
+
85
+ class PlaceholderClass
86
+ extend FastAttributes
87
+ attribute :value, Placeholder
88
+ attribute :title, Placeholder
89
+ end
90
+
91
+ FastAttributes.type_cast :lenient_attribute do
92
+ from '"yes"', to: 'true'
93
+ from '"no"', to: 'false'
94
+ otherwise 'nil'
95
+ end
96
+
97
+ class LenientAttributes
98
+ extend FastAttributes
99
+
100
+ attribute :terms_of_service, :lenient_attribute
101
+ end
102
+
103
+ class DefaultLenientAttributes
104
+ extend FastAttributes
105
+
106
+ attribute :title, :string
107
+ attribute :pages, :integer
108
+ attribute :price, :big_decimal
109
+ attribute :authors, :array
110
+ attribute :published, :date
111
+ attribute :sold, :time
112
+ attribute :finished, :date_time
113
+ attribute :rate, :float
114
+ attribute :active, :boolean
115
+ end
116
+
117
+ class AttributesWithoutAccessors
118
+ extend FastAttributes
119
+
120
+ define_attributes initialize: true, attributes: true do
121
+ attribute :title, :string
122
+ attribute :pages, :integer
123
+ end
124
+
125
+ def title
126
+ "Some title"
127
+ end
128
+
129
+ def attributes
130
+ super.merge('color' => 'white')
131
+ end
132
+ end
133
+
134
+ class AttributesWithAccessors
135
+ extend FastAttributes
136
+
137
+ define_attributes initialize: true, attributes: :accessors do
138
+ attribute :title, :string
139
+ attribute :pages, :integer
140
+ end
141
+
142
+ def attributes
143
+ super.merge('color' => 'white')
144
+ end
145
+
146
+ def pages
147
+ @pages + 10
148
+ end
149
+
150
+ def title
151
+ "A Longer Title: #{@title}"
152
+ end
153
+ end
154
+
155
+ class AttributesWithAccessorsAndDefaults
156
+ extend FastAttributes
157
+
158
+ define_attributes initialize: true, attributes: :accessors do
159
+ attribute :title, String, default: "a title"
160
+ attribute :pages, Integer, default: 10
161
+ end
162
+
163
+ def pages
164
+ @pages + 10
165
+ end
166
+ end
167
+
168
+ class ClassWithDefaults
169
+ extend FastAttributes
170
+
171
+ define_attributes initialize: true, attributes: true do
172
+ attribute :title, String, default: "a title"
173
+ attribute :pages, Integer, default: 10
174
+ attribute :authors, Array, default: [1, 2, 4]
175
+ attribute :callable, String, default: lambda { "callable value" }
176
+ end
177
+ end