grape-entity 0.1.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,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