fast_attributes_rails 1.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.
@@ -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