grape-entity 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,3 @@
1
+ module GrapeEntity
2
+ VERSION = '0.1.0'
3
+ end
@@ -0,0 +1,579 @@
1
+ require 'spec_helper'
2
+
3
+ describe GrapeEntity::Entity do
4
+ let(:fresh_class) { Class.new(GrapeEntity::Entity) }
5
+
6
+ context 'class methods' do
7
+ subject { fresh_class }
8
+
9
+ describe '.expose' do
10
+ context 'multiple attributes' do
11
+ it 'is able to add multiple exposed attributes with a single call' do
12
+ subject.expose :name, :email, :location
13
+ subject.exposures.size.should == 3
14
+ end
15
+
16
+ it 'sets the same options for all exposures passed' do
17
+ subject.expose :name, :email, :location, :foo => :bar
18
+ subject.exposures.values.each{|v| v.should == {:foo => :bar}}
19
+ end
20
+ end
21
+
22
+ context 'option validation' do
23
+ it 'makes sure that :as only works on single attribute calls' do
24
+ expect{ subject.expose :name, :email, :as => :foo }.to raise_error(ArgumentError)
25
+ expect{ subject.expose :name, :as => :foo }.not_to raise_error
26
+ end
27
+
28
+ it 'makes sure that :format_with as a proc can not be used with a block' do
29
+ expect { subject.expose :name, :format_with => Proc.new {} do |_| end }.to raise_error(ArgumentError)
30
+ end
31
+ end
32
+
33
+ context 'with a block' do
34
+ it 'errors out if called with multiple attributes' do
35
+ expect{ subject.expose(:name, :email) do
36
+ true
37
+ end }.to raise_error(ArgumentError)
38
+ end
39
+
40
+ it 'sets the :proc option in the exposure options' do
41
+ block = lambda{|_| true }
42
+ subject.expose :name, &block
43
+ subject.exposures[:name][:proc].should == block
44
+ end
45
+ end
46
+
47
+ context 'inherited exposures' do
48
+ it 'returns exposures from an ancestor' do
49
+ subject.expose :name, :email
50
+ child_class = Class.new(subject)
51
+
52
+ child_class.exposures.should eq(subject.exposures)
53
+ end
54
+
55
+ it 'returns exposures from multiple ancestor' do
56
+ subject.expose :name, :email
57
+ parent_class = Class.new(subject)
58
+ child_class = Class.new(parent_class)
59
+
60
+ child_class.exposures.should eq(subject.exposures)
61
+ end
62
+
63
+ it 'returns descendant exposures as a priority' do
64
+ subject.expose :name, :email
65
+ child_class = Class.new(subject)
66
+ child_class.expose :name do |_|
67
+ 'foo'
68
+ end
69
+
70
+ subject.exposures[:name].should_not have_key :proc
71
+ child_class.exposures[:name].should have_key :proc
72
+ end
73
+ end
74
+
75
+ context 'register formatters' do
76
+ let(:date_formatter) { lambda {|date| date.strftime('%m/%d/%Y') }}
77
+
78
+ it 'registers a formatter' do
79
+ subject.format_with :timestamp, &date_formatter
80
+
81
+ subject.formatters[:timestamp].should_not be_nil
82
+ end
83
+
84
+ it 'inherits formatters from ancestors' do
85
+ subject.format_with :timestamp, &date_formatter
86
+ child_class = Class.new(subject)
87
+
88
+ child_class.formatters.should == subject.formatters
89
+ end
90
+
91
+ it 'does not allow registering a formatter without a block' do
92
+ expect{ subject.format_with :foo }.to raise_error(ArgumentError)
93
+ end
94
+
95
+ it 'formats an exposure with a registered formatter' do
96
+ subject.format_with :timestamp do |date|
97
+ date.strftime('%m/%d/%Y')
98
+ end
99
+
100
+ subject.expose :birthday, :format_with => :timestamp
101
+
102
+ model = { :birthday => Time.gm(2012, 2, 27) }
103
+ subject.new(mock(model)).as_json[:birthday].should == '02/27/2012'
104
+ end
105
+ end
106
+ end
107
+
108
+ describe '.represent' do
109
+ it 'returns a single entity if called with one object' do
110
+ subject.represent(Object.new).should be_kind_of(subject)
111
+ end
112
+
113
+ it 'returns a single entity if called with a hash' do
114
+ subject.represent(Hash.new).should be_kind_of(subject)
115
+ end
116
+
117
+ it 'returns multiple entities if called with a collection' do
118
+ representation = subject.represent(4.times.map{Object.new})
119
+ representation.should be_kind_of Array
120
+ representation.size.should == 4
121
+ representation.reject{|r| r.kind_of?(subject)}.should be_empty
122
+ end
123
+
124
+ it 'adds the :collection => true option if called with a collection' do
125
+ representation = subject.represent(4.times.map{Object.new})
126
+ representation.each{|r| r.options[:collection].should be_true}
127
+ end
128
+ end
129
+
130
+ describe '.root' do
131
+ context 'with singular and plural root keys' do
132
+ before(:each) do
133
+ subject.root 'things', 'thing'
134
+ end
135
+
136
+ context 'with a single object' do
137
+ it 'allows a root element name to be specified' do
138
+ representation = subject.represent(Object.new)
139
+ representation.should be_kind_of Hash
140
+ representation.should have_key 'thing'
141
+ representation['thing'].should be_kind_of(subject)
142
+ end
143
+ end
144
+
145
+ context 'with an array of objects' do
146
+ it 'allows a root element name to be specified' do
147
+ representation = subject.represent(4.times.map{Object.new})
148
+ representation.should be_kind_of Hash
149
+ representation.should have_key 'things'
150
+ representation['things'].should be_kind_of Array
151
+ representation['things'].size.should == 4
152
+ representation['things'].reject{|r| r.kind_of?(subject)}.should be_empty
153
+ end
154
+ end
155
+
156
+ context 'it can be overridden' do
157
+ it 'can be disabled' do
158
+ representation = subject.represent(4.times.map{Object.new}, :root=>false)
159
+ representation.should be_kind_of Array
160
+ representation.size.should == 4
161
+ representation.reject{|r| r.kind_of?(subject)}.should be_empty
162
+ end
163
+ it 'can use a different name' do
164
+ representation = subject.represent(4.times.map{Object.new}, :root=>'others')
165
+ representation.should be_kind_of Hash
166
+ representation.should have_key 'others'
167
+ representation['others'].should be_kind_of Array
168
+ representation['others'].size.should == 4
169
+ representation['others'].reject{|r| r.kind_of?(subject)}.should be_empty
170
+ end
171
+ end
172
+ end
173
+
174
+ context 'with singular root key' do
175
+ before(:each) do
176
+ subject.root nil, 'thing'
177
+ end
178
+
179
+ context 'with a single object' do
180
+ it 'allows a root element name to be specified' do
181
+ representation = subject.represent(Object.new)
182
+ representation.should be_kind_of Hash
183
+ representation.should have_key 'thing'
184
+ representation['thing'].should be_kind_of(subject)
185
+ end
186
+ end
187
+
188
+ context 'with an array of objects' do
189
+ it 'allows a root element name to be specified' do
190
+ representation = subject.represent(4.times.map{Object.new})
191
+ representation.should be_kind_of Array
192
+ representation.size.should == 4
193
+ representation.reject{|r| r.kind_of?(subject)}.should be_empty
194
+ end
195
+ end
196
+ end
197
+
198
+ context 'with plural root key' do
199
+ before(:each) do
200
+ subject.root 'things'
201
+ end
202
+
203
+ context 'with a single object' do
204
+ it 'allows a root element name to be specified' do
205
+ subject.represent(Object.new).should be_kind_of(subject)
206
+ end
207
+ end
208
+
209
+ context 'with an array of objects' do
210
+ it 'allows a root element name to be specified' do
211
+ representation = subject.represent(4.times.map{Object.new})
212
+ representation.should be_kind_of Hash
213
+ representation.should have_key('things')
214
+ representation['things'].should be_kind_of Array
215
+ representation['things'].size.should == 4
216
+ representation['things'].reject{|r| r.kind_of?(subject)}.should be_empty
217
+ end
218
+ end
219
+ end
220
+ end
221
+
222
+ describe '#initialize' do
223
+ it 'takes an object and an optional options hash' do
224
+ expect{ subject.new(Object.new) }.not_to raise_error
225
+ expect{ subject.new }.to raise_error(ArgumentError)
226
+ expect{ subject.new(Object.new, {}) }.not_to raise_error
227
+ end
228
+
229
+ it 'has attribute readers for the object and options' do
230
+ entity = subject.new('abc', {})
231
+ entity.object.should == 'abc'
232
+ entity.options.should == {}
233
+ end
234
+ end
235
+ end
236
+
237
+ context 'instance methods' do
238
+
239
+ let(:model){ mock(attributes) }
240
+
241
+ let(:attributes) { {
242
+ :name => 'Bob Bobson',
243
+ :email => 'bob@example.com',
244
+ :birthday => Time.gm(2012, 2, 27),
245
+ :fantasies => ['Unicorns', 'Double Rainbows', 'Nessy'],
246
+ :friends => [
247
+ mock(:name => "Friend 1", :email => 'friend1@example.com', :fantasies => [], :birthday => Time.gm(2012, 2, 27), :friends => []),
248
+ mock(:name => "Friend 2", :email => 'friend2@example.com', :fantasies => [], :birthday => Time.gm(2012, 2, 27), :friends => [])
249
+ ]
250
+ } }
251
+
252
+ subject{ fresh_class.new(model) }
253
+
254
+ describe '#serializable_hash' do
255
+
256
+ it 'does not throw an exception if a nil options object is passed' do
257
+ expect{ fresh_class.new(model).serializable_hash(nil) }.not_to raise_error
258
+ end
259
+
260
+ it 'does not blow up when the model is nil' do
261
+ fresh_class.expose :name
262
+ expect{ fresh_class.new(nil).serializable_hash }.not_to raise_error
263
+ end
264
+
265
+ it 'does not throw an exception when an attribute is not found on the object' do
266
+ fresh_class.expose :name, :nonexistent_attribute
267
+ expect{ fresh_class.new(model).serializable_hash }.not_to raise_error
268
+ end
269
+
270
+ it "does not expose attributes that don't exist on the object" do
271
+ fresh_class.expose :email, :nonexistent_attribute, :name
272
+
273
+ res = fresh_class.new(model).serializable_hash
274
+ res.should have_key :email
275
+ res.should_not have_key :nonexistent_attribute
276
+ res.should have_key :name
277
+ end
278
+
279
+ it "does not expose attributes that don't exist on the object, even with criteria" do
280
+ fresh_class.expose :email
281
+ fresh_class.expose :nonexistent_attribute, :if => lambda { false }
282
+ fresh_class.expose :nonexistent_attribute2, :if => lambda { true }
283
+
284
+ res = fresh_class.new(model).serializable_hash
285
+ res.should have_key :email
286
+ res.should_not have_key :nonexistent_attribute
287
+ res.should_not have_key :nonexistent_attribute2
288
+ end
289
+
290
+ it "exposes attributes that don't exist on the object only when they are generated by a block" do
291
+ fresh_class.expose :nonexistent_attribute do |model, _|
292
+ "well, I do exist after all"
293
+ end
294
+ res = fresh_class.new(model).serializable_hash
295
+ res.should have_key :nonexistent_attribute
296
+ end
297
+
298
+ it "does not expose attributes that are generated by a block but have not passed criteria" do
299
+ fresh_class.expose :nonexistent_attribute, :proc => lambda {|model, _|
300
+ "I exist, but it is not yet my time to shine"
301
+ }, :if => lambda { |model, _| false }
302
+ res = fresh_class.new(model).serializable_hash
303
+ res.should_not have_key :nonexistent_attribute
304
+ end
305
+
306
+ context '#serializable_hash' do
307
+
308
+ module EntitySpec
309
+ class EmbeddedExample
310
+ def serializable_hash(opts = {})
311
+ { :abc => 'def' }
312
+ end
313
+ end
314
+ class EmbeddedExampleWithMany
315
+ def name
316
+ "abc"
317
+ end
318
+ def embedded
319
+ [ EmbeddedExample.new, EmbeddedExample.new ]
320
+ end
321
+ end
322
+ class EmbeddedExampleWithOne
323
+ def name
324
+ "abc"
325
+ end
326
+ def embedded
327
+ EmbeddedExample.new
328
+ end
329
+ end
330
+ end
331
+
332
+ it 'serializes embedded objects which respond to #serializable_hash' do
333
+ fresh_class.expose :name, :embedded
334
+ presenter = fresh_class.new(EntitySpec::EmbeddedExampleWithOne.new)
335
+ presenter.serializable_hash.should == {:name => "abc", :embedded => {:abc => "def"}}
336
+ end
337
+
338
+ it 'serializes embedded arrays of objects which respond to #serializable_hash' do
339
+ fresh_class.expose :name, :embedded
340
+ presenter = fresh_class.new(EntitySpec::EmbeddedExampleWithMany.new)
341
+ presenter.serializable_hash.should == {:name => "abc", :embedded => [{:abc => "def"}, {:abc => "def"}]}
342
+ end
343
+
344
+ end
345
+
346
+ end
347
+
348
+ describe '#value_for' do
349
+ before do
350
+ fresh_class.class_eval do
351
+ expose :name, :email
352
+ expose :friends, :using => self
353
+ expose :computed do |_, options|
354
+ options[:awesome]
355
+ end
356
+
357
+ expose :birthday, :format_with => :timestamp
358
+
359
+ def timestamp(date)
360
+ date.strftime('%m/%d/%Y')
361
+ end
362
+
363
+ expose :fantasies, :format_with => lambda {|f| f.reverse }
364
+ end
365
+ end
366
+
367
+ it 'passes through bare expose attributes' do
368
+ subject.send(:value_for, :name).should == attributes[:name]
369
+ end
370
+
371
+ it 'instantiates a representation if that is called for' do
372
+ rep = subject.send(:value_for, :friends)
373
+ rep.reject{|r| r.is_a?(fresh_class)}.should be_empty
374
+ rep.first.serializable_hash[:name].should == 'Friend 1'
375
+ rep.last.serializable_hash[:name].should == 'Friend 2'
376
+ end
377
+
378
+ context 'child representations' do
379
+ it 'disables root key name for child representations' do
380
+
381
+ module EntitySpec
382
+ class FriendEntity < GrapeEntity::Entity
383
+ root 'friends', 'friend'
384
+ expose :name, :email
385
+ end
386
+ end
387
+
388
+ fresh_class.class_eval do
389
+ expose :friends, :using => EntitySpec::FriendEntity
390
+ end
391
+
392
+ rep = subject.send(:value_for, :friends)
393
+ rep.should be_kind_of Array
394
+ rep.reject{|r| r.is_a?(EntitySpec::FriendEntity)}.should be_empty
395
+ rep.first.serializable_hash[:name].should == 'Friend 1'
396
+ rep.last.serializable_hash[:name].should == 'Friend 2'
397
+ end
398
+
399
+ it 'passes through custom options' do
400
+ module EntitySpec
401
+ class FriendEntity < GrapeEntity::Entity
402
+ root 'friends', 'friend'
403
+ expose :name
404
+ expose :email, :if => { :user_type => :admin }
405
+ end
406
+ end
407
+
408
+ fresh_class.class_eval do
409
+ expose :friends, :using => EntitySpec::FriendEntity
410
+ end
411
+
412
+ rep = subject.send(:value_for, :friends)
413
+ rep.should be_kind_of Array
414
+ rep.reject{|r| r.is_a?(EntitySpec::FriendEntity)}.should be_empty
415
+ rep.first.serializable_hash[:email].should be_nil
416
+ rep.last.serializable_hash[:email].should be_nil
417
+
418
+ rep = subject.send(:value_for, :friends, { :user_type => :admin })
419
+ rep.should be_kind_of Array
420
+ rep.reject{|r| r.is_a?(EntitySpec::FriendEntity)}.should be_empty
421
+ rep.first.serializable_hash[:email].should == 'friend1@example.com'
422
+ rep.last.serializable_hash[:email].should == 'friend2@example.com'
423
+ end
424
+
425
+ it 'ignores the :collection parameter in the source options' do
426
+ module EntitySpec
427
+ class FriendEntity < GrapeEntity::Entity
428
+ root 'friends', 'friend'
429
+ expose :name
430
+ expose :email, :if => { :collection => true }
431
+ end
432
+ end
433
+
434
+ fresh_class.class_eval do
435
+ expose :friends, :using => EntitySpec::FriendEntity
436
+ end
437
+
438
+ rep = subject.send(:value_for, :friends, { :collection => false })
439
+ rep.should be_kind_of Array
440
+ rep.reject{|r| r.is_a?(EntitySpec::FriendEntity)}.should be_empty
441
+ rep.first.serializable_hash[:email].should == 'friend1@example.com'
442
+ rep.last.serializable_hash[:email].should == 'friend2@example.com'
443
+ end
444
+
445
+ end
446
+
447
+ it 'calls through to the proc if there is one' do
448
+ subject.send(:value_for, :computed, :awesome => 123).should == 123
449
+ end
450
+
451
+ it 'returns a formatted value if format_with is passed' do
452
+ subject.send(:value_for, :birthday).should == '02/27/2012'
453
+ end
454
+
455
+ it 'returns a formatted value if format_with is passed a lambda' do
456
+ subject.send(:value_for, :fantasies).should == ['Nessy', 'Double Rainbows', 'Unicorns']
457
+ end
458
+ end
459
+
460
+ describe '#documentation' do
461
+ it 'returns an empty hash is no documentation is provided' do
462
+ fresh_class.expose :name
463
+
464
+ subject.documentation.should == {}
465
+ end
466
+
467
+ it 'returns each defined documentation hash' do
468
+ doc = {:type => "foo", :desc => "bar"}
469
+ fresh_class.expose :name, :documentation => doc
470
+ fresh_class.expose :email, :documentation => doc
471
+ fresh_class.expose :birthday
472
+
473
+ subject.documentation.should == {:name => doc, :email => doc}
474
+ end
475
+ end
476
+
477
+ describe '#key_for' do
478
+ it 'returns the attribute if no :as is set' do
479
+ fresh_class.expose :name
480
+ subject.send(:key_for, :name).should == :name
481
+ end
482
+
483
+ it 'returns a symbolized version of the attribute' do
484
+ fresh_class.expose :name
485
+ subject.send(:key_for, 'name').should == :name
486
+ end
487
+
488
+ it 'returns the :as alias if one exists' do
489
+ fresh_class.expose :name, :as => :nombre
490
+ subject.send(:key_for, 'name').should == :nombre
491
+ end
492
+ end
493
+
494
+ describe '#conditions_met?' do
495
+ it 'only passes through hash :if exposure if all attributes match' do
496
+ exposure_options = {:if => {:condition1 => true, :condition2 => true}}
497
+
498
+ subject.send(:conditions_met?, exposure_options, {}).should be_false
499
+ subject.send(:conditions_met?, exposure_options, :condition1 => true).should be_false
500
+ subject.send(:conditions_met?, exposure_options, :condition1 => true, :condition2 => true).should be_true
501
+ subject.send(:conditions_met?, exposure_options, :condition1 => false, :condition2 => true).should be_false
502
+ subject.send(:conditions_met?, exposure_options, :condition1 => true, :condition2 => true, :other => true).should be_true
503
+ end
504
+
505
+ it 'only passes through proc :if exposure if it returns truthy value' do
506
+ exposure_options = {:if => lambda{|_,opts| opts[:true]}}
507
+
508
+ subject.send(:conditions_met?, exposure_options, :true => false).should be_false
509
+ subject.send(:conditions_met?, exposure_options, :true => true).should be_true
510
+ end
511
+
512
+ it 'only passes through hash :unless exposure if any attributes do not match' do
513
+ exposure_options = {:unless => {:condition1 => true, :condition2 => true}}
514
+
515
+ subject.send(:conditions_met?, exposure_options, {}).should be_true
516
+ subject.send(:conditions_met?, exposure_options, :condition1 => true).should be_false
517
+ subject.send(:conditions_met?, exposure_options, :condition1 => true, :condition2 => true).should be_false
518
+ subject.send(:conditions_met?, exposure_options, :condition1 => false, :condition2 => true).should be_false
519
+ subject.send(:conditions_met?, exposure_options, :condition1 => true, :condition2 => true, :other => true).should be_false
520
+ subject.send(:conditions_met?, exposure_options, :condition1 => false, :condition2 => false).should be_true
521
+ end
522
+
523
+ it 'only passes through proc :unless exposure if it returns falsy value' do
524
+ exposure_options = {:unless => lambda{|_,options| options[:true] == true}}
525
+
526
+ subject.send(:conditions_met?, exposure_options, :true => false).should be_true
527
+ subject.send(:conditions_met?, exposure_options, :true => true).should be_false
528
+ end
529
+ end
530
+
531
+ describe '::DSL' do
532
+ subject{ Class.new }
533
+
534
+ it 'creates an Entity class when called' do
535
+ subject.should_not be_const_defined :Entity
536
+ subject.send(:include, GrapeEntity::Entity::DSL)
537
+ subject.should be_const_defined :Entity
538
+ end
539
+
540
+ context 'pre-mixed' do
541
+ before{ subject.send(:include, GrapeEntity::Entity::DSL) }
542
+
543
+ it 'is able to define entity traits through DSL' do
544
+ subject.entity do
545
+ expose :name
546
+ end
547
+
548
+ subject.entity_class.exposures.should_not be_empty
549
+ end
550
+
551
+ it 'is able to expose straight from the class' do
552
+ subject.entity :name, :email
553
+ subject.entity_class.exposures.size.should == 2
554
+ end
555
+
556
+ it 'is able to mix field and advanced exposures' do
557
+ subject.entity :name, :email do
558
+ expose :third
559
+ end
560
+ subject.entity_class.exposures.size.should == 3
561
+ end
562
+
563
+ context 'instance' do
564
+ let(:instance){ subject.new }
565
+
566
+ describe '#entity' do
567
+ it 'is an instance of the entity class' do
568
+ instance.entity.should be_kind_of(subject.entity_class)
569
+ end
570
+
571
+ it 'has an object of itself' do
572
+ instance.entity.object.should == instance
573
+ end
574
+ end
575
+ end
576
+ end
577
+ end
578
+ end
579
+ end