grape-entity 1.0.2 → 1.0.3

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.
@@ -1,2238 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'ostruct'
4
-
5
- describe Grape::Entity do
6
- let(:fresh_class) { Class.new(Grape::Entity) }
7
-
8
- context 'class methods' do
9
- subject { fresh_class }
10
-
11
- describe '.expose' do
12
- context 'multiple attributes' do
13
- it 'is able to add multiple exposed attributes with a single call' do
14
- subject.expose :name, :email, :location
15
- expect(subject.root_exposures.size).to eq 3
16
- end
17
-
18
- it 'sets the same options for all.root_exposures passed' do
19
- subject.expose :name, :email, :location, documentation: true
20
- subject.root_exposures.each { |v| expect(v.documentation).to eq true }
21
- end
22
- end
23
-
24
- context 'option validation' do
25
- it 'makes sure that :as only works on single attribute calls' do
26
- expect { subject.expose :name, :email, as: :foo }.to raise_error ArgumentError
27
- expect { subject.expose :name, as: :foo }.not_to raise_error
28
- end
29
-
30
- it 'makes sure that :format_with as a proc cannot be used with a block' do
31
- # rubocop:disable Style/BlockDelimiters
32
- expect {
33
- subject.expose :name, format_with: proc {} do
34
- p 'hi'
35
- end
36
- }.to raise_error ArgumentError
37
- # rubocop:enable Style/BlockDelimiters
38
- end
39
-
40
- it 'makes sure unknown options are not silently ignored' do
41
- expect { subject.expose :name, unknown: nil }.to raise_error ArgumentError
42
- end
43
- end
44
-
45
- context 'with a :merge option' do
46
- let(:nested_hash) do
47
- { something: { like_nested_hash: true }, special: { like_nested_hash: '12' } }
48
- end
49
-
50
- it 'merges an exposure to the root' do
51
- subject.expose(:something, merge: true)
52
- expect(subject.represent(nested_hash).serializable_hash).to eq(nested_hash[:something])
53
- end
54
-
55
- it 'allows to solve collisions providing a lambda to a :merge option' do
56
- subject.expose(:something, merge: true)
57
- subject.expose(:special, merge: ->(_, v1, v2) { v1 && v2 ? 'brand new val' : v2 })
58
- expect(subject.represent(nested_hash).serializable_hash).to eq(like_nested_hash: 'brand new val')
59
- end
60
-
61
- context 'and nested object is nil' do
62
- let(:nested_hash) do
63
- { something: nil, special: { like_nested_hash: '12' } }
64
- end
65
-
66
- it 'adds nothing to output' do
67
- subject.expose(:something, merge: true)
68
- subject.expose(:special)
69
- expect(subject.represent(nested_hash).serializable_hash).to eq(special: { like_nested_hash: '12' })
70
- end
71
- end
72
- end
73
-
74
- context 'with :expose_nil option' do
75
- let(:a) { nil }
76
- let(:b) { nil }
77
- let(:c) { 'value' }
78
-
79
- context 'when model is a PORO' do
80
- let(:model) { Model.new(a, b, c) }
81
-
82
- before do
83
- stub_const 'Model', Class.new
84
- Model.class_eval do
85
- attr_accessor :a, :b, :c
86
-
87
- def initialize(a, b, c)
88
- @a = a
89
- @b = b
90
- @c = c
91
- end
92
- end
93
- end
94
-
95
- context 'when expose_nil option is not provided' do
96
- it 'exposes nil attributes' do
97
- subject.expose(:a)
98
- subject.expose(:b)
99
- subject.expose(:c)
100
- expect(subject.represent(model).serializable_hash).to eq(a: nil, b: nil, c: 'value')
101
- end
102
- end
103
-
104
- context 'when expose_nil option is true' do
105
- it 'exposes nil attributes' do
106
- subject.expose(:a, expose_nil: true)
107
- subject.expose(:b, expose_nil: true)
108
- subject.expose(:c)
109
- expect(subject.represent(model).serializable_hash).to eq(a: nil, b: nil, c: 'value')
110
- end
111
- end
112
-
113
- context 'when expose_nil option is false' do
114
- it 'does not expose nil attributes' do
115
- subject.expose(:a, expose_nil: false)
116
- subject.expose(:b, expose_nil: false)
117
- subject.expose(:c)
118
- expect(subject.represent(model).serializable_hash).to eq(c: 'value')
119
- end
120
-
121
- it 'is only applied per attribute' do
122
- subject.expose(:a, expose_nil: false)
123
- subject.expose(:b)
124
- subject.expose(:c)
125
- expect(subject.represent(model).serializable_hash).to eq(b: nil, c: 'value')
126
- end
127
-
128
- it 'raises an error when applied to multiple attribute exposures' do
129
- expect { subject.expose(:a, :b, :c, expose_nil: false) }.to raise_error ArgumentError
130
- end
131
- end
132
-
133
- context 'when expose_nil option is false and block passed' do
134
- it 'does not expose if block returns nil' do
135
- subject.expose(:a, expose_nil: false) do |_obj, options|
136
- options[:option_a]
137
- end
138
- subject.expose(:b)
139
- subject.expose(:c)
140
- expect(subject.represent(model).serializable_hash).to eq(b: nil, c: 'value')
141
- end
142
-
143
- it 'exposes is block returns a value' do
144
- subject.expose(:a, expose_nil: false) do |_obj, options|
145
- options[:option_a]
146
- end
147
- subject.expose(:b)
148
- subject.expose(:c)
149
- expect(subject.represent(model, option_a: 100).serializable_hash).to eq(a: 100, b: nil, c: 'value')
150
- end
151
- end
152
- end
153
-
154
- context 'when model is a hash' do
155
- let(:model) { { a: a, b: b, c: c } }
156
-
157
- context 'when expose_nil option is not provided' do
158
- it 'exposes nil attributes' do
159
- subject.expose(:a)
160
- subject.expose(:b)
161
- subject.expose(:c)
162
- expect(subject.represent(model).serializable_hash).to eq(a: nil, b: nil, c: 'value')
163
- end
164
- end
165
-
166
- context 'when expose_nil option is true' do
167
- it 'exposes nil attributes' do
168
- subject.expose(:a, expose_nil: true)
169
- subject.expose(:b, expose_nil: true)
170
- subject.expose(:c)
171
- expect(subject.represent(model).serializable_hash).to eq(a: nil, b: nil, c: 'value')
172
- end
173
- end
174
-
175
- context 'when expose_nil option is false' do
176
- it 'does not expose nil attributes' do
177
- subject.expose(:a, expose_nil: false)
178
- subject.expose(:b, expose_nil: false)
179
- subject.expose(:c)
180
- expect(subject.represent(model).serializable_hash).to eq(c: 'value')
181
- end
182
-
183
- it 'is only applied per attribute' do
184
- subject.expose(:a, expose_nil: false)
185
- subject.expose(:b)
186
- subject.expose(:c)
187
- expect(subject.represent(model).serializable_hash).to eq(b: nil, c: 'value')
188
- end
189
-
190
- it 'raises an error when applied to multiple attribute exposures' do
191
- expect { subject.expose(:a, :b, :c, expose_nil: false) }.to raise_error ArgumentError
192
- end
193
- end
194
- end
195
-
196
- context 'with nested structures' do
197
- let(:model) { { a: a, b: b, c: { d: nil, e: nil, f: { g: nil, h: nil } } } }
198
-
199
- context 'when expose_nil option is false' do
200
- it 'does not expose nil attributes' do
201
- subject.expose(:a, expose_nil: false)
202
- subject.expose(:b)
203
- subject.expose(:c) do
204
- subject.expose(:d, expose_nil: false)
205
- subject.expose(:e)
206
- subject.expose(:f) do
207
- subject.expose(:g, expose_nil: false)
208
- subject.expose(:h)
209
- end
210
- end
211
-
212
- expect(subject.represent(model).serializable_hash).to eq(b: nil, c: { e: nil, f: { h: nil } })
213
- end
214
- end
215
- end
216
- end
217
-
218
- context 'with :default option' do
219
- let(:a) { nil }
220
- let(:b) { nil }
221
- let(:c) { 'value' }
222
-
223
- context 'when model is a PORO' do
224
- let(:model) { Model.new(a, b, c) }
225
-
226
- before do
227
- stub_const 'Model', Class.new
228
- Model.class_eval do
229
- attr_accessor :a, :b, :c
230
-
231
- def initialize(a, b, c)
232
- @a = a
233
- @b = b
234
- @c = c
235
- end
236
- end
237
- end
238
-
239
- context 'when default option is not provided' do
240
- it 'exposes attributes values' do
241
- subject.expose(:a)
242
- subject.expose(:b)
243
- subject.expose(:c)
244
- expect(subject.represent(model).serializable_hash).to eq(a: nil, b: nil, c: 'value')
245
- end
246
- end
247
-
248
- context 'when default option is set' do
249
- it 'exposes default values for attributes' do
250
- subject.expose(:a, default: 'a')
251
- subject.expose(:b, default: false)
252
- subject.expose(:c, default: 'c')
253
- expect(subject.represent(model).serializable_hash).to eq(a: 'a', b: false, c: 'value')
254
- end
255
- end
256
-
257
- context 'when default option is set and block passed' do
258
- it 'return default value if block returns nil' do
259
- subject.expose(:a, default: 'a') do |_obj, _options|
260
- nil
261
- end
262
- subject.expose(:b)
263
- subject.expose(:c)
264
- expect(subject.represent(model).serializable_hash).to eq(a: 'a', b: nil, c: 'value')
265
- end
266
-
267
- it 'return value from block if block returns a value' do
268
- subject.expose(:a, default: 'a') do |_obj, _options|
269
- 100
270
- end
271
- subject.expose(:b)
272
- subject.expose(:c)
273
- expect(subject.represent(model).serializable_hash).to eq(a: 100, b: nil, c: 'value')
274
- end
275
- end
276
- end
277
-
278
- context 'when model is a hash' do
279
- let(:model) { { a: a, b: b, c: c } }
280
-
281
- context 'when expose_nil option is not provided' do
282
- it 'exposes nil attributes' do
283
- subject.expose(:a)
284
- subject.expose(:b)
285
- subject.expose(:c)
286
- expect(subject.represent(model).serializable_hash).to eq(a: nil, b: nil, c: 'value')
287
- end
288
- end
289
-
290
- context 'when expose_nil option is true' do
291
- it 'exposes nil attributes' do
292
- subject.expose(:a, expose_nil: true)
293
- subject.expose(:b, expose_nil: true)
294
- subject.expose(:c)
295
- expect(subject.represent(model).serializable_hash).to eq(a: nil, b: nil, c: 'value')
296
- end
297
- end
298
-
299
- context 'when expose_nil option is false' do
300
- it 'does not expose nil attributes' do
301
- subject.expose(:a, expose_nil: false)
302
- subject.expose(:b, expose_nil: false)
303
- subject.expose(:c)
304
- expect(subject.represent(model).serializable_hash).to eq(c: 'value')
305
- end
306
-
307
- it 'is only applied per attribute' do
308
- subject.expose(:a, expose_nil: false)
309
- subject.expose(:b)
310
- subject.expose(:c)
311
- expect(subject.represent(model).serializable_hash).to eq(b: nil, c: 'value')
312
- end
313
-
314
- it 'raises an error when applied to multiple attribute exposures' do
315
- expect { subject.expose(:a, :b, :c, expose_nil: false) }.to raise_error ArgumentError
316
- end
317
- end
318
- end
319
-
320
- context 'with nested structures' do
321
- let(:model) { { a: a, b: b, c: { d: nil, e: nil, f: { g: nil, h: nil } } } }
322
-
323
- context 'when expose_nil option is false' do
324
- it 'does not expose nil attributes' do
325
- subject.expose(:a, expose_nil: false)
326
- subject.expose(:b)
327
- subject.expose(:c) do
328
- subject.expose(:d, expose_nil: false)
329
- subject.expose(:e)
330
- subject.expose(:f) do
331
- subject.expose(:g, expose_nil: false)
332
- subject.expose(:h)
333
- end
334
- end
335
-
336
- expect(subject.represent(model).serializable_hash).to eq(b: nil, c: { e: nil, f: { h: nil } })
337
- end
338
- end
339
- end
340
- end
341
-
342
- context 'with a block' do
343
- it 'errors out if called with multiple attributes' do
344
- expect { subject.expose(:name, :email) { true } }.to raise_error ArgumentError
345
- end
346
-
347
- it 'references an instance of the entity with :using option' do
348
- module EntitySpec
349
- class SomeObject1
350
- attr_accessor :prop1
351
-
352
- def initialize
353
- @prop1 = 'value1'
354
- end
355
- end
356
-
357
- class BogusEntity < Grape::Entity
358
- expose :prop1
359
- end
360
- end
361
-
362
- subject.expose(:bogus, using: EntitySpec::BogusEntity) do |entity|
363
- entity.prop1 = 'MODIFIED 2'
364
- entity
365
- end
366
-
367
- object = EntitySpec::SomeObject1.new
368
- value = subject.represent(object).value_for(:bogus)
369
- expect(value).to be_instance_of EntitySpec::BogusEntity
370
-
371
- prop1 = value.value_for(:prop1)
372
- expect(prop1).to eq 'MODIFIED 2'
373
- end
374
-
375
- context 'with parameters passed to the block' do
376
- it 'sets the :proc option in the exposure options' do
377
- block = ->(_) { true }
378
- subject.expose :name, using: 'Awesome', &block
379
- exposure = subject.find_exposure(:name)
380
- expect(exposure.subexposure.block).to eq(block)
381
- expect(exposure.using_class_name).to eq('Awesome')
382
- end
383
-
384
- it 'references an instance of the entity without any options' do
385
- subject.expose(:size) { |_| self }
386
- expect(subject.represent({}).value_for(:size)).to be_an_instance_of fresh_class
387
- end
388
- end
389
-
390
- describe 'blocks' do
391
- class SomeObject
392
- def method_without_args
393
- 'result'
394
- end
395
-
396
- def method_with_one_arg(_object)
397
- 'result'
398
- end
399
-
400
- def method_with_multiple_args(_object, _options)
401
- 'result'
402
- end
403
-
404
- def raises_argument_error
405
- raise ArgumentError, 'something different'
406
- end
407
- end
408
-
409
- describe 'with block passed in' do
410
- specify do
411
- subject.expose :that_method_without_args do |object|
412
- object.method_without_args
413
- end
414
-
415
- object = SomeObject.new
416
-
417
- value = subject.represent(object).value_for(:that_method_without_args)
418
- expect(value).to eq('result')
419
- end
420
-
421
- it 'does not suppress ArgumentError' do
422
- subject.expose :raises_argument_error do |object|
423
- object.raises_argument_error
424
- end
425
-
426
- object = SomeObject.new
427
- expect do
428
- subject.represent(object).value_for(:raises_argument_error)
429
- end.to raise_error(ArgumentError, 'something different')
430
- end
431
- end
432
-
433
- context 'with block passed in via &' do
434
- specify do
435
- subject.expose :that_method_without_args, &:method_without_args
436
- subject.expose :method_without_args, as: :that_method_without_args_again
437
-
438
- object = SomeObject.new
439
-
440
- value = subject.represent(object).value_for(:method_without_args)
441
- expect(value).to be_nil
442
-
443
- value = subject.represent(object).value_for(:that_method_without_args)
444
- expect(value).to eq('result')
445
-
446
- value = subject.represent(object).value_for(:that_method_without_args_again)
447
- expect(value).to eq('result')
448
- end
449
- end
450
-
451
- context 'with block passed in via &' do
452
- specify do
453
- subject.expose :that_method_with_one_arg, &:method_with_one_arg
454
- subject.expose :that_method_with_multple_args, &:method_with_multiple_args
455
-
456
- object = SomeObject.new
457
-
458
- expect do
459
- subject.represent(object).value_for(:that_method_with_one_arg)
460
- end.to raise_error ArgumentError, match(/method expects 1 argument/)
461
-
462
- expect do
463
- subject.represent(object).value_for(:that_method_with_multple_args)
464
- end.to raise_error ArgumentError, match(/method expects 2 arguments/)
465
- end
466
- end
467
-
468
- context 'with symbol-to-proc passed in via &' do
469
- specify do
470
- subject.expose :that_undefined_method, &:unknown_method
471
-
472
- object = SomeObject.new
473
-
474
- expect do
475
- subject.represent(object).value_for(:that_undefined_method)
476
- end.to raise_error ArgumentError, match(/method is not defined in the object/)
477
- end
478
- end
479
- end
480
-
481
- context 'with no parameters passed to the block' do
482
- it 'adds a nested exposure' do
483
- subject.expose :awesome do
484
- subject.expose :nested do
485
- subject.expose :moar_nested, as: 'weee'
486
- end
487
- subject.expose :another_nested, using: 'Awesome'
488
- end
489
-
490
- awesome = subject.find_exposure(:awesome)
491
- nested = awesome.find_nested_exposure(:nested)
492
- another_nested = awesome.find_nested_exposure(:another_nested)
493
- moar_nested = nested.find_nested_exposure(:moar_nested)
494
-
495
- expect(awesome).to be_nesting
496
- expect(nested).to_not be_nil
497
- expect(another_nested).to_not be_nil
498
- expect(another_nested.using_class_name).to eq('Awesome')
499
- expect(moar_nested).to_not be_nil
500
- expect(moar_nested.key(subject)).to eq(:weee)
501
- end
502
-
503
- it 'represents the exposure as a hash of its nested.root_exposures' do
504
- subject.expose :awesome do
505
- subject.expose(:nested) { |_| 'value' }
506
- subject.expose(:another_nested) { |_| 'value' }
507
- end
508
-
509
- expect(subject.represent({}).value_for(:awesome)).to eq(
510
- nested: 'value',
511
- another_nested: 'value'
512
- )
513
- end
514
-
515
- it 'does not represent nested.root_exposures whose conditions are not met' do
516
- subject.expose :awesome do
517
- subject.expose(:condition_met, if: ->(_, _) { true }) { |_| 'value' }
518
- subject.expose(:condition_not_met, if: ->(_, _) { false }) { |_| 'value' }
519
- end
520
-
521
- expect(subject.represent({}).value_for(:awesome)).to eq(condition_met: 'value')
522
- end
523
-
524
- it 'does not represent attributes, declared inside nested exposure, outside of it' do
525
- subject.expose :awesome do
526
- subject.expose(:nested) { |_| 'value' }
527
- subject.expose(:another_nested) { |_| 'value' }
528
- subject.expose :second_level_nested do
529
- subject.expose(:deeply_exposed_attr) { |_| 'value' }
530
- end
531
- end
532
-
533
- expect(subject.represent({}).serializable_hash).to eq(
534
- awesome: {
535
- nested: 'value',
536
- another_nested: 'value',
537
- second_level_nested: {
538
- deeply_exposed_attr: 'value'
539
- }
540
- }
541
- )
542
- end
543
-
544
- it 'merges complex nested attributes' do
545
- class ClassRoom < Grape::Entity
546
- expose(:parents, using: 'Parent') { |_| [{}, {}] }
547
- end
548
-
549
- class Person < Grape::Entity
550
- expose :user do
551
- expose(:in_first) { |_| 'value' }
552
- end
553
- end
554
-
555
- class Student < Person
556
- expose :user do
557
- expose(:user_id) { |_| 'value' }
558
- expose(:user_display_id, as: :display_id) { |_| 'value' }
559
- end
560
- end
561
-
562
- class Parent < Person
563
- expose(:children, using: 'Student') { |_| [{}, {}] }
564
- end
565
-
566
- expect(ClassRoom.represent({}).serializable_hash).to eq(
567
- parents: [
568
- {
569
- user: { in_first: 'value' },
570
- children: [
571
- { user: { in_first: 'value', user_id: 'value', display_id: 'value' } },
572
- { user: { in_first: 'value', user_id: 'value', display_id: 'value' } }
573
- ]
574
- },
575
- {
576
- user: { in_first: 'value' },
577
- children: [
578
- { user: { in_first: 'value', user_id: 'value', display_id: 'value' } },
579
- { user: { in_first: 'value', user_id: 'value', display_id: 'value' } }
580
- ]
581
- }
582
- ]
583
- )
584
- end
585
-
586
- it 'merges results of deeply nested double.root_exposures inside of nesting exposure' do
587
- entity = Class.new(Grape::Entity) do
588
- expose :data do
589
- expose :something do
590
- expose(:x) { |_| 'x' }
591
- end
592
- expose :something do
593
- expose(:y) { |_| 'y' }
594
- end
595
- end
596
- end
597
- expect(entity.represent({}).serializable_hash).to eq(
598
- data: {
599
- something: {
600
- x: 'x',
601
- y: 'y'
602
- }
603
- }
604
- )
605
- end
606
-
607
- it 'serializes deeply nested presenter exposures' do
608
- e = Class.new(Grape::Entity) do
609
- expose :f
610
- end
611
- subject.expose :a do
612
- subject.expose :b do
613
- subject.expose :c do
614
- subject.expose :lol, using: e
615
- end
616
- end
617
- end
618
- expect(subject.represent(lol: { f: 123 }).serializable_hash).to eq(
619
- a: { b: { c: { lol: { f: 123 } } } }
620
- )
621
- end
622
-
623
- it 'is safe if its nested.root_exposures are safe' do
624
- subject.with_options safe: true do
625
- subject.expose :awesome do
626
- subject.expose(:nested) { |_| 'value' }
627
- end
628
- subject.expose :not_awesome do
629
- subject.expose :nested
630
- end
631
- end
632
- expect(subject.represent({}, serializable: true)).to eq(
633
- awesome: {
634
- nested: 'value'
635
- },
636
- not_awesome: {
637
- nested: nil
638
- }
639
- )
640
- end
641
- it 'merges attriutes if :merge option is passed' do
642
- user_entity = Class.new(Grape::Entity)
643
- admin_entity = Class.new(Grape::Entity)
644
- user_entity.expose(:id, :name)
645
- admin_entity.expose(:id, :name)
646
-
647
- subject.expose(:profiles) do
648
- subject.expose(:users, merge: true, using: user_entity)
649
- subject.expose(:admins, merge: true, using: admin_entity)
650
- end
651
-
652
- subject.expose :awesome do
653
- subject.expose(:nested, merge: true) { |_| { just_a_key: 'value' } }
654
- subject.expose(:another_nested, merge: true) { |_| { just_another_key: 'value' } }
655
- end
656
-
657
- additional_hash = { users: [{ id: 1, name: 'John' }, { id: 2, name: 'Jay' }],
658
- admins: [{ id: 3, name: 'Jack' }, { id: 4, name: 'James' }] }
659
- expect(subject.represent(additional_hash).serializable_hash).to eq(
660
- profiles: additional_hash[:users] + additional_hash[:admins],
661
- awesome: { just_a_key: 'value', just_another_key: 'value' }
662
- )
663
- end
664
- end
665
- end
666
-
667
- context 'inherited.root_exposures' do
668
- it 'returns.root_exposures from an ancestor' do
669
- subject.expose :name, :email
670
- child_class = Class.new(subject)
671
-
672
- expect(child_class.root_exposures).to eq(subject.root_exposures)
673
- end
674
-
675
- it 'returns.root_exposures from multiple ancestor' do
676
- subject.expose :name, :email
677
- parent_class = Class.new(subject)
678
- child_class = Class.new(parent_class)
679
-
680
- expect(child_class.root_exposures).to eq(subject.root_exposures)
681
- end
682
-
683
- it 'returns descendant.root_exposures as a priority' do
684
- subject.expose :name, :email
685
- child_class = Class.new(subject)
686
- child_class.expose :name do |_|
687
- 'foo'
688
- end
689
-
690
- expect(subject.represent({ name: 'bar' }, serializable: true)).to eq(email: nil, name: 'bar')
691
- expect(child_class.represent({ name: 'bar' }, serializable: true)).to eq(email: nil, name: 'foo')
692
- end
693
-
694
- it 'not overrides exposure by default' do
695
- subject.expose :name
696
- child_class = Class.new(subject)
697
- child_class.expose :name, as: :child_name
698
-
699
- expect(subject.represent({ name: 'bar' }, serializable: true)).to eq(name: 'bar')
700
- expect(child_class.represent({ name: 'bar' }, serializable: true)).to eq(name: 'bar', child_name: 'bar')
701
- end
702
-
703
- it 'overrides parent class exposure when option is specified' do
704
- subject.expose :name
705
- child_class = Class.new(subject)
706
- child_class.expose :name, as: :child_name, override: true
707
-
708
- expect(subject.represent({ name: 'bar' }, serializable: true)).to eq(name: 'bar')
709
- expect(child_class.represent({ name: 'bar' }, serializable: true)).to eq(child_name: 'bar')
710
- end
711
- end
712
-
713
- context 'register formatters' do
714
- let(:date_formatter) { ->(date) { date.strftime('%m/%d/%Y') } }
715
-
716
- it 'registers a formatter' do
717
- subject.format_with :timestamp, &date_formatter
718
-
719
- expect(subject.formatters[:timestamp]).not_to be_nil
720
- end
721
-
722
- it 'inherits formatters from ancestors' do
723
- subject.format_with :timestamp, &date_formatter
724
- child_class = Class.new(subject)
725
-
726
- expect(child_class.formatters).to eq subject.formatters
727
- end
728
-
729
- it 'does not allow registering a formatter without a block' do
730
- expect { subject.format_with :foo }.to raise_error ArgumentError
731
- end
732
-
733
- it 'formats an exposure with a registered formatter' do
734
- subject.format_with :timestamp do |date|
735
- date.strftime('%m/%d/%Y')
736
- end
737
-
738
- subject.expose :birthday, format_with: :timestamp
739
-
740
- model = { birthday: Time.gm(2012, 2, 27) }
741
- expect(subject.new(double(model)).as_json[:birthday]).to eq '02/27/2012'
742
- end
743
-
744
- it 'formats an exposure with a :format_with lambda that returns a value from the entity instance' do
745
- object = {}
746
-
747
- subject.expose(:size, format_with: ->(_value) { object.class.to_s })
748
- expect(subject.represent(object).value_for(:size)).to eq object.class.to_s
749
- end
750
-
751
- it 'formats an exposure with a :format_with symbol that returns a value from the entity instance' do
752
- subject.format_with :size_formatter do |_date|
753
- object.class.to_s
754
- end
755
-
756
- object = {}
757
-
758
- subject.expose(:size, format_with: :size_formatter)
759
- expect(subject.represent(object).value_for(:size)).to eq object.class.to_s
760
- end
761
-
762
- it 'works global on Grape::Entity' do
763
- Grape::Entity.format_with :size_formatter do |_date|
764
- object.class.to_s
765
- end
766
- object = {}
767
-
768
- subject.expose(:size, format_with: :size_formatter)
769
- expect(subject.represent(object).value_for(:size)).to eq object.class.to_s
770
- end
771
- end
772
-
773
- it 'works global on Grape::Entity' do
774
- Grape::Entity.expose :a
775
- object = { a: 11, b: 22 }
776
- expect(Grape::Entity.represent(object).value_for(:a)).to eq 11
777
- subject.expose :b
778
- expect(subject.represent(object).value_for(:a)).to eq 11
779
- expect(subject.represent(object).value_for(:b)).to eq 22
780
- Grape::Entity.unexpose :a
781
- end
782
- end
783
-
784
- describe '.unexpose' do
785
- it 'is able to remove exposed attributes' do
786
- subject.expose :name, :email
787
- subject.unexpose :email
788
-
789
- expect(subject.root_exposures.length).to eq 1
790
- expect(subject.root_exposures[0].attribute).to eq :name
791
- end
792
-
793
- context 'inherited.root_exposures' do
794
- it 'when called from child class, only removes from the attribute from child' do
795
- subject.expose :name, :email
796
- child_class = Class.new(subject)
797
- child_class.unexpose :email
798
-
799
- expect(child_class.root_exposures.length).to eq 1
800
- expect(child_class.root_exposures[0].attribute).to eq :name
801
- expect(subject.root_exposures[0].attribute).to eq :name
802
- expect(subject.root_exposures[1].attribute).to eq :email
803
- end
804
-
805
- context 'when called from the parent class' do
806
- it 'remove from parent and do not remove from child classes' do
807
- subject.expose :name, :email
808
- child_class = Class.new(subject)
809
- subject.unexpose :email
810
-
811
- expect(subject.root_exposures.length).to eq 1
812
- expect(subject.root_exposures[0].attribute).to eq :name
813
- expect(child_class.root_exposures[0].attribute).to eq :name
814
- expect(child_class.root_exposures[1].attribute).to eq :email
815
- end
816
- end
817
- end
818
-
819
- it 'does not allow unexposing inside of nesting exposures' do
820
- expect do
821
- Class.new(Grape::Entity) do
822
- expose :something do
823
- expose :x
824
- unexpose :x
825
- end
826
- end
827
- end.to raise_error(/You cannot call 'unexpose`/)
828
- end
829
-
830
- it 'works global on Grape::Entity' do
831
- Grape::Entity.expose :x
832
- expect(Grape::Entity.root_exposures[0].attribute).to eq(:x)
833
- Grape::Entity.unexpose :x
834
- expect(Grape::Entity.root_exposures).to eq([])
835
- end
836
- end
837
-
838
- describe '.with_options' do
839
- it 'raises an error for unknown options' do
840
- block = proc do
841
- with_options(unknown: true) do
842
- expose :awesome_thing
843
- end
844
- end
845
-
846
- expect { subject.class_eval(&block) }.to raise_error ArgumentError
847
- end
848
-
849
- it 'applies the options to all.root_exposures inside' do
850
- subject.class_eval do
851
- with_options(if: { awesome: true }) do
852
- expose :awesome_thing, using: 'Awesome'
853
- end
854
- end
855
-
856
- exposure = subject.find_exposure(:awesome_thing)
857
- expect(exposure.using_class_name).to eq('Awesome')
858
- expect(exposure.conditions[0].cond_hash).to eq(awesome: true)
859
- end
860
-
861
- it 'allows for nested .with_options' do
862
- subject.class_eval do
863
- with_options(if: { awesome: true }) do
864
- with_options(using: 'Something') do
865
- expose :awesome_thing
866
- end
867
- end
868
- end
869
-
870
- exposure = subject.find_exposure(:awesome_thing)
871
- expect(exposure.using_class_name).to eq('Something')
872
- expect(exposure.conditions[0].cond_hash).to eq(awesome: true)
873
- end
874
-
875
- it 'overrides nested :as option' do
876
- subject.class_eval do
877
- with_options(as: :sweet) do
878
- expose :awesome_thing, as: :extra_smooth
879
- end
880
- end
881
-
882
- exposure = subject.find_exposure(:awesome_thing)
883
- expect(exposure.key(subject)).to eq :extra_smooth
884
- end
885
-
886
- it 'merges nested :if option' do
887
- match_proc = ->(_obj, _opts) { true }
888
-
889
- subject.class_eval do
890
- # Symbol
891
- with_options(if: :awesome) do
892
- # Hash
893
- with_options(if: { awesome: true }) do
894
- # Proc
895
- with_options(if: match_proc) do
896
- # Hash (override existing key and merge new key)
897
- with_options(if: { awesome: false, less_awesome: true }) do
898
- expose :awesome_thing
899
- end
900
- end
901
- end
902
- end
903
- end
904
-
905
- exposure = subject.find_exposure(:awesome_thing)
906
- expect(exposure.conditions.any?(&:inversed?)).to be_falsey
907
- expect(exposure.conditions[0].symbol).to eq(:awesome)
908
- expect(exposure.conditions[1].block).to eq(match_proc)
909
- expect(exposure.conditions[2].cond_hash).to eq(awesome: false, less_awesome: true)
910
- end
911
-
912
- it 'merges nested :unless option' do
913
- match_proc = ->(_, _) { true }
914
-
915
- subject.class_eval do
916
- # Symbol
917
- with_options(unless: :awesome) do
918
- # Hash
919
- with_options(unless: { awesome: true }) do
920
- # Proc
921
- with_options(unless: match_proc) do
922
- # Hash (override existing key and merge new key)
923
- with_options(unless: { awesome: false, less_awesome: true }) do
924
- expose :awesome_thing
925
- end
926
- end
927
- end
928
- end
929
- end
930
-
931
- exposure = subject.find_exposure(:awesome_thing)
932
- expect(exposure.conditions.all?(&:inversed?)).to be_truthy
933
- expect(exposure.conditions[0].symbol).to eq(:awesome)
934
- expect(exposure.conditions[1].block).to eq(match_proc)
935
- expect(exposure.conditions[2].cond_hash).to eq(awesome: false, less_awesome: true)
936
- end
937
-
938
- it 'overrides nested :using option' do
939
- subject.class_eval do
940
- with_options(using: 'Something') do
941
- expose :awesome_thing, using: 'SomethingElse'
942
- end
943
- end
944
-
945
- exposure = subject.find_exposure(:awesome_thing)
946
- expect(exposure.using_class_name).to eq('SomethingElse')
947
- end
948
-
949
- it 'aliases :with option to :using option' do
950
- subject.class_eval do
951
- with_options(using: 'Something') do
952
- expose :awesome_thing, with: 'SomethingElse'
953
- end
954
- end
955
-
956
- exposure = subject.find_exposure(:awesome_thing)
957
- expect(exposure.using_class_name).to eq('SomethingElse')
958
- end
959
-
960
- it 'overrides nested :proc option' do
961
- match_proc = ->(_obj, _opts) { 'more awesomer' }
962
-
963
- subject.class_eval do
964
- with_options(proc: ->(_obj, _opts) { 'awesome' }) do
965
- expose :awesome_thing, proc: match_proc
966
- end
967
- end
968
-
969
- exposure = subject.find_exposure(:awesome_thing)
970
- expect(exposure.block).to eq(match_proc)
971
- end
972
-
973
- it 'overrides nested :documentation option' do
974
- subject.class_eval do
975
- with_options(documentation: { desc: 'Description.' }) do
976
- expose :awesome_thing, documentation: { desc: 'Other description.' }
977
- end
978
- end
979
-
980
- exposure = subject.find_exposure(:awesome_thing)
981
- expect(exposure.documentation).to eq(desc: 'Other description.')
982
- end
983
-
984
- it 'propagates expose_nil option' do
985
- subject.class_eval do
986
- with_options(expose_nil: false) do
987
- expose :awesome_thing
988
- end
989
- end
990
-
991
- exposure = subject.find_exposure(:awesome_thing)
992
- expect(exposure.conditions[0].inversed?).to be true
993
- expect(exposure.conditions[0].block.call(awesome_thing: nil)).to be true
994
- end
995
-
996
- it 'overrides nested :expose_nil option' do
997
- subject.class_eval do
998
- with_options(expose_nil: true) do
999
- expose :awesome_thing, expose_nil: false
1000
- expose :other_awesome_thing
1001
- end
1002
- end
1003
-
1004
- exposure = subject.find_exposure(:awesome_thing)
1005
- expect(exposure.conditions[0].inversed?).to be true
1006
- expect(exposure.conditions[0].block.call(awesome_thing: nil)).to be true
1007
- # Conditions are only added for exposures that do not expose nil
1008
- exposure = subject.find_exposure(:other_awesome_thing)
1009
- expect(exposure.conditions[0]).to be_nil
1010
- end
1011
- end
1012
-
1013
- describe '.[]' do
1014
- it 'returns the input unchanged' do
1015
- hash = { name: 'Test' }
1016
- expect(subject[hash]).to eq(hash)
1017
- end
1018
-
1019
- it 'returns nil unchanged' do
1020
- expect(subject[nil]).to be_nil
1021
- end
1022
-
1023
- it 'is inherited by subclasses' do
1024
- subclass = Class.new(subject)
1025
- expect(subclass[{ id: 1 }]).to eq(id: 1)
1026
- end
1027
- end
1028
-
1029
- describe '.represent' do
1030
- it 'returns a single entity if called with one object' do
1031
- expect(subject.represent(Object.new)).to be_kind_of(subject)
1032
- end
1033
-
1034
- it 'returns a single entity if called with a hash' do
1035
- expect(subject.represent({})).to be_kind_of(subject)
1036
- end
1037
-
1038
- it 'returns multiple entities if called with a collection' do
1039
- representation = subject.represent(Array.new(4) { Object.new })
1040
- expect(representation).to be_kind_of Array
1041
- expect(representation.size).to eq(4)
1042
- expect(representation.grep_v(subject)).to be_empty
1043
- end
1044
-
1045
- it 'adds the collection: true option if called with a collection' do
1046
- representation = subject.represent(Array.new(4) { Object.new })
1047
- representation.each { |r| expect(r.options[:collection]).to be true }
1048
- end
1049
-
1050
- it 'returns a serialized hash of a single object if serializable: true' do
1051
- subject.expose(:awesome) { |_| true }
1052
- representation = subject.represent(Object.new, serializable: true)
1053
- expect(representation).to eq(awesome: true)
1054
- end
1055
-
1056
- it 'returns a serialized array of hashes of multiple objects if serializable: true' do
1057
- subject.expose(:awesome) { |_| true }
1058
- representation = subject.represent(Array.new(2) { Object.new }, serializable: true)
1059
- expect(representation).to eq([{ awesome: true }, { awesome: true }])
1060
- end
1061
-
1062
- it 'returns a serialized hash of a hash' do
1063
- subject.expose(:awesome)
1064
- representation = subject.represent({ awesome: true }, serializable: true)
1065
- expect(representation).to eq(awesome: true)
1066
- end
1067
-
1068
- it 'returns a serialized hash of an OpenStruct' do
1069
- subject.expose(:awesome)
1070
- representation = subject.represent(OpenStruct.new, serializable: true)
1071
- expect(representation).to eq(awesome: nil)
1072
- end
1073
-
1074
- it 'raises error if field not found' do
1075
- subject.expose(:awesome)
1076
- expect do
1077
- subject.represent(Object.new, serializable: true)
1078
- end.to raise_error(NoMethodError, /missing attribute `awesome'/)
1079
- end
1080
-
1081
- context 'with specified fields' do
1082
- it 'returns only specified fields with only option' do
1083
- subject.expose(:id, :name, :phone)
1084
- representation = subject.represent(OpenStruct.new, only: %i[id name], serializable: true)
1085
- expect(representation).to eq(id: nil, name: nil)
1086
- end
1087
-
1088
- it 'returns all fields except the ones specified in the except option' do
1089
- subject.expose(:id, :name, :phone)
1090
- representation = subject.represent(OpenStruct.new, except: [:phone], serializable: true)
1091
- expect(representation).to eq(id: nil, name: nil)
1092
- end
1093
-
1094
- it 'returns only fields specified in the only option and not specified in the except option' do
1095
- subject.expose(:id, :name, :phone)
1096
- representation = subject.represent(OpenStruct.new,
1097
- only: %i[name phone],
1098
- except: [:phone], serializable: true)
1099
- expect(representation).to eq(name: nil)
1100
- end
1101
-
1102
- context 'with strings or symbols passed to only and except' do
1103
- let(:object) { OpenStruct.new(user: {}) }
1104
-
1105
- before do
1106
- user_entity = Class.new(Grape::Entity)
1107
- user_entity.expose(:id, :name, :email)
1108
-
1109
- subject.expose(:id, :name, :phone, :address)
1110
- subject.expose(:user, using: user_entity)
1111
- end
1112
-
1113
- it 'can specify "only" option attributes as strings' do
1114
- representation = subject.represent(object, only: ['id', 'name', { 'user' => ['email'] }], serializable: true)
1115
- expect(representation).to eq(id: nil, name: nil, user: { email: nil })
1116
- end
1117
-
1118
- it 'can specify "except" option attributes as strings' do
1119
- representation = subject.represent(object, except: ['id', 'name', { 'user' => ['email'] }], serializable: true)
1120
- expect(representation).to eq(phone: nil, address: nil, user: { id: nil, name: nil })
1121
- end
1122
-
1123
- it 'can specify "only" option attributes as symbols' do
1124
- representation = subject.represent(object, only: [:name, :phone, { user: [:name] }], serializable: true)
1125
- expect(representation).to eq(name: nil, phone: nil, user: { name: nil })
1126
- end
1127
-
1128
- it 'can specify "except" option attributes as symbols' do
1129
- representation = subject.represent(object, except: [:name, :phone, { user: [:name] }], serializable: true)
1130
- expect(representation).to eq(id: nil, address: nil, user: { id: nil, email: nil })
1131
- end
1132
-
1133
- it 'can specify "only" attributes as strings and symbols' do
1134
- representation = subject.represent(object, only: [:id, 'address', { user: [:id, 'name'] }], serializable: true)
1135
- expect(representation).to eq(id: nil, address: nil, user: { id: nil, name: nil })
1136
- end
1137
-
1138
- it 'can specify "except" attributes as strings and symbols' do
1139
- representation = subject.represent(object, except: [:id, 'address', { user: [:id, 'name'] }], serializable: true)
1140
- expect(representation).to eq(name: nil, phone: nil, user: { email: nil })
1141
- end
1142
-
1143
- context 'with nested attributes' do
1144
- before do
1145
- subject.expose :additional do
1146
- subject.expose :something
1147
- end
1148
- end
1149
-
1150
- it 'preserves nesting' do
1151
- expect(subject.represent({ something: 123 }, only: [{ additional: [:something] }], serializable: true)).to eq(
1152
- additional: {
1153
- something: 123
1154
- }
1155
- )
1156
- end
1157
- end
1158
- end
1159
-
1160
- it 'can specify children attributes with only' do
1161
- user_entity = Class.new(Grape::Entity)
1162
- user_entity.expose(:id, :name, :email)
1163
-
1164
- subject.expose(:id, :name, :phone)
1165
- subject.expose(:user, using: user_entity)
1166
-
1167
- representation = subject.represent(OpenStruct.new(user: {}), only: [:id, :name, { user: %i[name email] }], serializable: true)
1168
- expect(representation).to eq(id: nil, name: nil, user: { name: nil, email: nil })
1169
- end
1170
-
1171
- it 'can specify children attributes with except' do
1172
- user_entity = Class.new(Grape::Entity)
1173
- user_entity.expose(:id, :name, :email)
1174
-
1175
- subject.expose(:id, :name, :phone)
1176
- subject.expose(:user, using: user_entity)
1177
-
1178
- representation = subject.represent(OpenStruct.new(user: {}), except: [:phone, { user: [:id] }], serializable: true)
1179
- expect(representation).to eq(id: nil, name: nil, user: { name: nil, email: nil })
1180
- end
1181
-
1182
- it 'can specify children attributes with mixed only and except' do
1183
- user_entity = Class.new(Grape::Entity)
1184
- user_entity.expose(:id, :name, :email, :address)
1185
-
1186
- subject.expose(:id, :name, :phone, :mobile_phone)
1187
- subject.expose(:user, using: user_entity)
1188
-
1189
- representation = subject.represent(OpenStruct.new(user: {}),
1190
- only: [:id, :name, :phone, { user: %i[id name email] }],
1191
- except: [:phone, { user: [:id] }], serializable: true)
1192
- expect(representation).to eq(id: nil, name: nil, user: { name: nil, email: nil })
1193
- end
1194
-
1195
- context 'specify attribute with exposure condition' do
1196
- it 'returns only specified fields' do
1197
- subject.expose(:id)
1198
- subject.with_options(if: { condition: true }) do
1199
- subject.expose(:name)
1200
- end
1201
-
1202
- representation = subject.represent(OpenStruct.new, condition: true, only: %i[id name], serializable: true)
1203
- expect(representation).to eq(id: nil, name: nil)
1204
- end
1205
-
1206
- it 'does not return fields specified in the except option' do
1207
- subject.expose(:id, :phone)
1208
- subject.with_options(if: { condition: true }) do
1209
- subject.expose(:name, :mobile_phone)
1210
- end
1211
-
1212
- representation = subject.represent(OpenStruct.new, condition: true, except: %i[phone mobile_phone], serializable: true)
1213
- expect(representation).to eq(id: nil, name: nil)
1214
- end
1215
-
1216
- it 'choses proper exposure according to condition' do
1217
- strategy1 = ->(_obj, _opts) { 'foo' }
1218
- strategy2 = ->(_obj, _opts) { 'bar' }
1219
-
1220
- subject.expose :id, proc: strategy1
1221
- subject.expose :id, proc: strategy2
1222
- expect(subject.represent({}, condition: false, serializable: true)).to eq(id: 'bar')
1223
- expect(subject.represent({}, condition: true, serializable: true)).to eq(id: 'bar')
1224
-
1225
- subject.unexpose_all
1226
-
1227
- subject.expose :id, proc: strategy1, if: :condition
1228
- subject.expose :id, proc: strategy2
1229
- expect(subject.represent({}, condition: false, serializable: true)).to eq(id: 'bar')
1230
- expect(subject.represent({}, condition: true, serializable: true)).to eq(id: 'bar')
1231
-
1232
- subject.unexpose_all
1233
-
1234
- subject.expose :id, proc: strategy1
1235
- subject.expose :id, proc: strategy2, if: :condition
1236
- expect(subject.represent({}, condition: false, serializable: true)).to eq(id: 'foo')
1237
- expect(subject.represent({}, condition: true, serializable: true)).to eq(id: 'bar')
1238
-
1239
- subject.unexpose_all
1240
-
1241
- subject.expose :id, proc: strategy1, if: :condition1
1242
- subject.expose :id, proc: strategy2, if: :condition2
1243
- expect(subject.represent({}, condition1: false, condition2: false, serializable: true)).to eq({})
1244
- expect(subject.represent({}, condition1: false, condition2: true, serializable: true)).to eq(id: 'bar')
1245
- expect(subject.represent({}, condition1: true, condition2: false, serializable: true)).to eq(id: 'foo')
1246
- expect(subject.represent({}, condition1: true, condition2: true, serializable: true)).to eq(id: 'bar')
1247
- end
1248
-
1249
- it 'does not merge nested exposures with plain hashes' do
1250
- subject.expose(:id)
1251
- subject.expose(:info, if: :condition1) do
1252
- subject.expose :a, :b
1253
- subject.expose(:additional, if: :condition2) do |_obj, _opts|
1254
- {
1255
- x: 11, y: 22, c: 123
1256
- }
1257
- end
1258
- end
1259
- subject.expose(:info, if: :condition2) do
1260
- subject.expose(:additional) do
1261
- subject.expose :c
1262
- end
1263
- end
1264
- subject.expose(:d, as: :info, if: :condition3)
1265
-
1266
- obj = { id: 123, a: 1, b: 2, c: 3, d: 4 }
1267
-
1268
- expect(subject.represent(obj, serializable: true)).to eq(id: 123)
1269
- expect(subject.represent(obj, condition1: true, serializable: true)).to eq(id: 123, info: { a: 1, b: 2 })
1270
- expect(subject.represent(obj, condition2: true, serializable: true)).to eq(
1271
- id: 123,
1272
- info: {
1273
- additional: {
1274
- c: 3
1275
- }
1276
- }
1277
- )
1278
- expect(subject.represent(obj, condition1: true, condition2: true, serializable: true)).to eq(
1279
- id: 123,
1280
- info: {
1281
- a: 1, b: 2, additional: { c: 3 }
1282
- }
1283
- )
1284
- expect(subject.represent(obj, condition3: true, serializable: true)).to eq(id: 123, info: 4)
1285
- expect(subject.represent(obj, condition1: true, condition2: true, condition3: true, serializable: true)).to eq(id: 123, info: 4)
1286
- end
1287
- end
1288
-
1289
- context 'attribute with alias' do
1290
- it 'returns only specified fields' do
1291
- subject.expose(:id)
1292
- subject.expose(:name, as: :title)
1293
-
1294
- representation = subject.represent(OpenStruct.new, condition: true, only: %i[id title], serializable: true)
1295
- expect(representation).to eq(id: nil, title: nil)
1296
- end
1297
-
1298
- it 'does not return fields specified in the except option' do
1299
- subject.expose(:id)
1300
- subject.expose(:name, as: :title)
1301
- subject.expose(:phone, as: :phone_number)
1302
-
1303
- representation = subject.represent(OpenStruct.new, condition: true, except: [:phone_number], serializable: true)
1304
- expect(representation).to eq(id: nil, title: nil)
1305
- end
1306
- end
1307
-
1308
- context 'attribute that is an entity itself' do
1309
- it 'returns correctly the children entity attributes' do
1310
- user_entity = Class.new(Grape::Entity)
1311
- user_entity.expose(:id, :name, :email)
1312
-
1313
- nephew_entity = Class.new(Grape::Entity)
1314
- nephew_entity.expose(:id, :name, :email)
1315
-
1316
- subject.expose(:id, :name, :phone)
1317
- subject.expose(:user, using: user_entity)
1318
- subject.expose(:nephew, using: nephew_entity)
1319
-
1320
- representation = subject.represent(OpenStruct.new(user: {}),
1321
- only: %i[id name user], except: [:nephew], serializable: true)
1322
- expect(representation).to eq(id: nil, name: nil, user: { id: nil, name: nil, email: nil })
1323
- end
1324
- end
1325
-
1326
- context 'when NameError happens in a parameterized block_exposure' do
1327
- before do
1328
- subject.expose :raise_no_method_error do |_|
1329
- foo
1330
- end
1331
- end
1332
-
1333
- it 'does not cause infinite loop' do
1334
- expect { subject.represent({}, serializable: true) }.to raise_error(NameError)
1335
- end
1336
- end
1337
- end
1338
- end
1339
-
1340
- describe '.present_collection' do
1341
- it 'make the objects accessible' do
1342
- subject.present_collection true
1343
- subject.expose :items
1344
-
1345
- representation = subject.represent(Array.new(4) { Object.new })
1346
- expect(representation).to be_kind_of(subject)
1347
- expect(representation.object).to be_kind_of(Hash)
1348
- expect(representation.object).to have_key :items
1349
- expect(representation.object[:items]).to be_kind_of Array
1350
- expect(representation.object[:items].size).to be 4
1351
- end
1352
-
1353
- it 'serializes items with my root name' do
1354
- subject.present_collection true, :my_items
1355
- subject.expose :my_items
1356
-
1357
- representation = subject.represent(Array.new(4) { Object.new }, serializable: true)
1358
- expect(representation).to be_kind_of(Grape::Entity::Exposure::NestingExposure::OutputBuilder)
1359
- expect(representation).to be_kind_of(Hash)
1360
- expect(representation).to have_key :my_items
1361
- expect(representation[:my_items]).to be_kind_of Array
1362
- expect(representation[:my_items].size).to be 4
1363
- end
1364
- end
1365
-
1366
- describe '.root' do
1367
- context 'with singular and plural root keys' do
1368
- before(:each) do
1369
- subject.root 'things', 'thing'
1370
- end
1371
-
1372
- context 'with a single object' do
1373
- it 'allows a root element name to be specified' do
1374
- representation = subject.represent(Object.new)
1375
- expect(representation).to be_kind_of Hash
1376
- expect(representation).to have_key 'thing'
1377
- expect(representation['thing']).to be_kind_of(subject)
1378
- end
1379
- end
1380
-
1381
- context 'with an array of objects' do
1382
- it 'allows a root element name to be specified' do
1383
- representation = subject.represent(Array.new(4) { Object.new })
1384
- expect(representation).to be_kind_of Hash
1385
- expect(representation).to have_key 'things'
1386
- expect(representation['things']).to be_kind_of Array
1387
- expect(representation['things'].size).to eq 4
1388
- expect(representation['things'].grep_v(subject)).to be_empty
1389
- end
1390
- end
1391
-
1392
- context 'it can be overridden' do
1393
- it 'can be disabled' do
1394
- representation = subject.represent(Array.new(4) { Object.new }, root: false)
1395
- expect(representation).to be_kind_of Array
1396
- expect(representation.size).to eq 4
1397
- expect(representation.grep_v(subject)).to be_empty
1398
- end
1399
- it 'can use a different name' do
1400
- representation = subject.represent(Array.new(4) { Object.new }, root: 'others')
1401
- expect(representation).to be_kind_of Hash
1402
- expect(representation).to have_key 'others'
1403
- expect(representation['others']).to be_kind_of Array
1404
- expect(representation['others'].size).to eq 4
1405
- expect(representation['others'].grep_v(subject)).to be_empty
1406
- end
1407
- end
1408
- end
1409
-
1410
- context 'with singular root key' do
1411
- before(:each) do
1412
- subject.root nil, 'thing'
1413
- end
1414
-
1415
- context 'with a single object' do
1416
- it 'allows a root element name to be specified' do
1417
- representation = subject.represent(Object.new)
1418
- expect(representation).to be_kind_of Hash
1419
- expect(representation).to have_key 'thing'
1420
- expect(representation['thing']).to be_kind_of(subject)
1421
- end
1422
- end
1423
-
1424
- context 'with an array of objects' do
1425
- it 'allows a root element name to be specified' do
1426
- representation = subject.represent(Array.new(4) { Object.new })
1427
- expect(representation).to be_kind_of Array
1428
- expect(representation.size).to eq 4
1429
- expect(representation.grep_v(subject)).to be_empty
1430
- end
1431
- end
1432
- end
1433
-
1434
- context 'with plural root key' do
1435
- before(:each) do
1436
- subject.root 'things'
1437
- end
1438
-
1439
- context 'with a single object' do
1440
- it 'allows a root element name to be specified' do
1441
- expect(subject.represent(Object.new)).to be_kind_of(subject)
1442
- end
1443
- end
1444
-
1445
- context 'with an array of objects' do
1446
- it 'allows a root element name to be specified' do
1447
- representation = subject.represent(Array.new(4) { Object.new })
1448
- expect(representation).to be_kind_of Hash
1449
- expect(representation).to have_key('things')
1450
- expect(representation['things']).to be_kind_of Array
1451
- expect(representation['things'].size).to eq 4
1452
- expect(representation['things'].grep_v(subject)).to be_empty
1453
- end
1454
- end
1455
- end
1456
-
1457
- context 'inheriting from parent entity' do
1458
- before(:each) do
1459
- subject.root 'things', 'thing'
1460
- end
1461
-
1462
- it 'inherits single root' do
1463
- child_class = Class.new(subject)
1464
- representation = child_class.represent(Object.new)
1465
- expect(representation).to be_kind_of Hash
1466
- expect(representation).to have_key 'thing'
1467
- expect(representation['thing']).to be_kind_of(child_class)
1468
- end
1469
-
1470
- it 'inherits array root root' do
1471
- child_class = Class.new(subject)
1472
- representation = child_class.represent(Array.new(4) { Object.new })
1473
- expect(representation).to be_kind_of Hash
1474
- expect(representation).to have_key('things')
1475
- expect(representation['things']).to be_kind_of Array
1476
- expect(representation['things'].size).to eq 4
1477
- expect(representation['things'].grep_v(child_class)).to be_empty
1478
- end
1479
- end
1480
- end
1481
-
1482
- describe '#initialize' do
1483
- it 'takes an object and an optional options hash' do
1484
- expect { subject.new(Object.new) }.not_to raise_error
1485
- expect { subject.new }.to raise_error ArgumentError
1486
- expect { subject.new(Object.new, {}) }.not_to raise_error
1487
- end
1488
-
1489
- it 'has attribute readers for the object and options' do
1490
- entity = subject.new('abc', {})
1491
- expect(entity.object).to eq 'abc'
1492
- expect(entity.options).to eq({})
1493
- end
1494
- end
1495
- end
1496
-
1497
- context 'instance methods' do
1498
- let(:model) { double(attributes) }
1499
-
1500
- let(:attributes) do
1501
- {
1502
- name: 'Bob Bobson',
1503
- email: 'bob@example.com',
1504
- birthday: Time.gm(2012, 2, 27),
1505
- fantasies: ['Unicorns', 'Double Rainbows', 'Nessy'],
1506
- characteristics: [
1507
- { key: 'hair_color', value: 'brown' }
1508
- ],
1509
- friends: [
1510
- double(name: 'Friend 1', email: 'friend1@example.com', characteristics: [], fantasies: [], birthday: Time.gm(2012, 2, 27), friends: []),
1511
- double(name: 'Friend 2', email: 'friend2@example.com', characteristics: [], fantasies: [], birthday: Time.gm(2012, 2, 27), friends: [])
1512
- ],
1513
- extra: { key: 'foo', value: 'bar' },
1514
- nested: [
1515
- { name: 'n1', data: { key: 'ex1', value: 'v1' } },
1516
- { name: 'n2', data: { key: 'ex2', value: 'v2' } }
1517
- ]
1518
- }
1519
- end
1520
-
1521
- subject { fresh_class.new(model) }
1522
-
1523
- describe '#serializable_hash' do
1524
- it 'does not throw an exception if a nil options object is passed' do
1525
- expect { fresh_class.new(model).serializable_hash(nil) }.not_to raise_error
1526
- end
1527
-
1528
- it 'does not blow up when the model is nil' do
1529
- fresh_class.expose :name
1530
- expect { fresh_class.new(nil).serializable_hash }.not_to raise_error
1531
- end
1532
-
1533
- context 'with safe option' do
1534
- it 'does not throw an exception when an attribute is not found on the object' do
1535
- fresh_class.expose :name, :nonexistent_attribute, safe: true
1536
- expect { fresh_class.new(model).serializable_hash }.not_to raise_error
1537
- end
1538
-
1539
- it 'exposes values of private method calls' do
1540
- some_class = Class.new do
1541
- define_method :name do
1542
- true
1543
- end
1544
- private :name
1545
- end
1546
- fresh_class.expose :name, safe: true
1547
- expect(fresh_class.new(some_class.new).serializable_hash).to eq(name: true)
1548
- end
1549
-
1550
- it "does expose attributes that don't exist on the object" do
1551
- fresh_class.expose :email, :nonexistent_attribute, :name, safe: true
1552
-
1553
- res = fresh_class.new(model).serializable_hash
1554
- expect(res).to have_key :email
1555
- expect(res).to have_key :nonexistent_attribute
1556
- expect(res).to have_key :name
1557
- end
1558
-
1559
- it "does expose attributes that don't exist on the object as nil" do
1560
- fresh_class.expose :email, :nonexistent_attribute, :name, safe: true
1561
-
1562
- res = fresh_class.new(model).serializable_hash
1563
- expect(res[:nonexistent_attribute]).to eq(nil)
1564
- end
1565
-
1566
- it 'does expose attributes marked as safe if model is a hash object' do
1567
- fresh_class.expose :name, safe: true
1568
-
1569
- res = fresh_class.new(name: 'myname').serializable_hash
1570
- expect(res).to have_key :name
1571
- end
1572
-
1573
- it "does expose attributes that don't exist on the object as nil if criteria is true" do
1574
- fresh_class.expose :email
1575
- fresh_class.expose :nonexistent_attribute, safe: true, if: ->(_obj, _opts) { false }
1576
- fresh_class.expose :nonexistent_attribute2, safe: true, if: ->(_obj, _opts) { true }
1577
-
1578
- res = fresh_class.new(model).serializable_hash
1579
- expect(res).to have_key :email
1580
- expect(res).not_to have_key :nonexistent_attribute
1581
- expect(res).to have_key :nonexistent_attribute2
1582
- end
1583
- end
1584
-
1585
- context 'without safe option' do
1586
- it 'throws an exception when an attribute is not found on the object' do
1587
- fresh_class.expose :name, :nonexistent_attribute
1588
- expect { fresh_class.new(model).serializable_hash }.to raise_error NoMethodError
1589
- end
1590
-
1591
- it "exposes attributes that don't exist on the object only when they are generated by a block" do
1592
- fresh_class.expose :nonexistent_attribute do |_model, _opts|
1593
- 'well, I do exist after all'
1594
- end
1595
- res = fresh_class.new(model).serializable_hash
1596
- expect(res).to have_key :nonexistent_attribute
1597
- end
1598
-
1599
- it 'does not expose attributes that are generated by a block but have not passed criteria' do
1600
- fresh_class.expose :nonexistent_attribute,
1601
- proc: ->(_model, _opts) { 'I exist, but it is not yet my time to shine' },
1602
- if: ->(_model, _opts) { false }
1603
- res = fresh_class.new(model).serializable_hash
1604
- expect(res).not_to have_key :nonexistent_attribute
1605
- end
1606
- end
1607
-
1608
- it "exposes attributes that don't exist on the object only when they are generated by a block with options" do
1609
- module EntitySpec
1610
- class TestEntity < Grape::Entity
1611
- end
1612
- end
1613
-
1614
- fresh_class.expose :nonexistent_attribute, using: EntitySpec::TestEntity do |_model, _opts|
1615
- 'well, I do exist after all'
1616
- end
1617
- res = fresh_class.new(model).serializable_hash
1618
- expect(res).to have_key :nonexistent_attribute
1619
- end
1620
-
1621
- it 'exposes attributes defined through module inclusion' do
1622
- module SharedAttributes
1623
- def a_value
1624
- 3.14
1625
- end
1626
- end
1627
- fresh_class.include(SharedAttributes)
1628
- fresh_class.expose :a_value
1629
- res = fresh_class.new(model).serializable_hash
1630
- expect(res[:a_value]).to eq(3.14)
1631
- end
1632
-
1633
- it 'does not expose attributes that are generated by a block but have not passed criteria' do
1634
- fresh_class.expose :nonexistent_attribute,
1635
- proc: ->(_, _) { 'I exist, but it is not yet my time to shine' },
1636
- if: ->(_, _) { false }
1637
- res = fresh_class.new(model).serializable_hash
1638
- expect(res).not_to have_key :nonexistent_attribute
1639
- end
1640
-
1641
- context '#serializable_hash' do
1642
- module EntitySpec
1643
- class EmbeddedExample
1644
- def serializable_hash(_opts = {})
1645
- { abc: 'def' }
1646
- end
1647
- end
1648
-
1649
- class EmbeddedExampleWithHash
1650
- def name
1651
- 'abc'
1652
- end
1653
-
1654
- def embedded
1655
- { a: nil, b: EmbeddedExample.new }
1656
- end
1657
- end
1658
-
1659
- class EmbeddedExampleWithMany
1660
- def name
1661
- 'abc'
1662
- end
1663
-
1664
- def embedded
1665
- [EmbeddedExample.new, EmbeddedExample.new]
1666
- end
1667
- end
1668
-
1669
- class EmbeddedExampleWithOne
1670
- def name
1671
- 'abc'
1672
- end
1673
-
1674
- def embedded
1675
- EmbeddedExample.new
1676
- end
1677
- end
1678
- end
1679
-
1680
- it 'serializes embedded objects which respond to #serializable_hash' do
1681
- fresh_class.expose :name, :embedded
1682
- presenter = fresh_class.new(EntitySpec::EmbeddedExampleWithOne.new)
1683
- expect(presenter.serializable_hash).to eq(name: 'abc', embedded: { abc: 'def' })
1684
- end
1685
-
1686
- it 'serializes embedded arrays of objects which respond to #serializable_hash' do
1687
- fresh_class.expose :name, :embedded
1688
- presenter = fresh_class.new(EntitySpec::EmbeddedExampleWithMany.new)
1689
- expect(presenter.serializable_hash).to eq(name: 'abc', embedded: [{ abc: 'def' }, { abc: 'def' }])
1690
- end
1691
-
1692
- it 'serializes embedded hashes of objects which respond to #serializable_hash' do
1693
- fresh_class.expose :name, :embedded
1694
- presenter = fresh_class.new(EntitySpec::EmbeddedExampleWithHash.new)
1695
- expect(presenter.serializable_hash).to eq(name: 'abc', embedded: { a: nil, b: { abc: 'def' } })
1696
- end
1697
- end
1698
-
1699
- context '#attr_path' do
1700
- it 'for all kinds of attributes' do
1701
- module EntitySpec
1702
- class EmailEntity < Grape::Entity
1703
- expose(:email, as: :addr) { |_, o| o[:attr_path].join('/') }
1704
- end
1705
-
1706
- class UserEntity < Grape::Entity
1707
- expose(:name, as: :full_name) { |_, o| o[:attr_path].join('/') }
1708
- expose :email, using: 'EntitySpec::EmailEntity'
1709
- end
1710
-
1711
- class ExtraEntity < Grape::Entity
1712
- expose(:key) { |_, o| o[:attr_path].join('/') }
1713
- expose(:value) { |_, o| o[:attr_path].join('/') }
1714
- end
1715
-
1716
- class NestedEntity < Grape::Entity
1717
- expose(:name) { |_, o| o[:attr_path].join('/') }
1718
- expose :data, using: 'EntitySpec::ExtraEntity'
1719
- end
1720
- end
1721
-
1722
- fresh_class.class_eval do
1723
- expose(:id) { |_, o| o[:attr_path].join('/') }
1724
- expose(:foo, as: :bar) { |_, o| o[:attr_path].join('/') }
1725
- expose :title do
1726
- expose :full do
1727
- expose(:prefix, as: :pref) { |_, o| o[:attr_path].join('/') }
1728
- expose(:main) { |_, o| o[:attr_path].join('/') }
1729
- end
1730
- end
1731
- expose :friends, as: :social, using: 'EntitySpec::UserEntity'
1732
- expose :extra, using: 'EntitySpec::ExtraEntity'
1733
- expose :nested, using: 'EntitySpec::NestedEntity'
1734
- end
1735
-
1736
- expect(subject.serializable_hash).to eq(
1737
- id: 'id',
1738
- bar: 'bar',
1739
- title: { full: { pref: 'title/full/pref', main: 'title/full/main' } },
1740
- social: [
1741
- { full_name: 'social/full_name', email: { addr: 'social/email/addr' } },
1742
- { full_name: 'social/full_name', email: { addr: 'social/email/addr' } }
1743
- ],
1744
- extra: { key: 'extra/key', value: 'extra/value' },
1745
- nested: [
1746
- { name: 'nested/name', data: { key: 'nested/data/key', value: 'nested/data/value' } },
1747
- { name: 'nested/name', data: { key: 'nested/data/key', value: 'nested/data/value' } }
1748
- ]
1749
- )
1750
- end
1751
-
1752
- it 'allows customize path of an attribute' do
1753
- module EntitySpec
1754
- class CharacterEntity < Grape::Entity
1755
- expose(:key) { |_, o| o[:attr_path].join('/') }
1756
- expose(:value) { |_, o| o[:attr_path].join('/') }
1757
- end
1758
- end
1759
-
1760
- fresh_class.class_eval do
1761
- expose :characteristics, using: EntitySpec::CharacterEntity,
1762
- attr_path: ->(_obj, _opts) { :character }
1763
- end
1764
-
1765
- expect(subject.serializable_hash).to eq(
1766
- characteristics: [
1767
- { key: 'character/key', value: 'character/value' }
1768
- ]
1769
- )
1770
- end
1771
-
1772
- it 'can drop one nest level by set path_for to nil' do
1773
- module EntitySpec
1774
- class NoPathCharacterEntity < Grape::Entity
1775
- expose(:key) { |_, o| o[:attr_path].join('/') }
1776
- expose(:value) { |_, o| o[:attr_path].join('/') }
1777
- end
1778
- end
1779
-
1780
- fresh_class.class_eval do
1781
- expose :characteristics, using: EntitySpec::NoPathCharacterEntity, attr_path: proc {}
1782
- end
1783
-
1784
- expect(subject.serializable_hash).to eq(
1785
- characteristics: [
1786
- { key: 'key', value: 'value' }
1787
- ]
1788
- )
1789
- end
1790
- end
1791
-
1792
- context 'with projections passed in options' do
1793
- it 'allows to pass different :only and :except params using the same instance' do
1794
- fresh_class.expose :a, :b, :c
1795
- presenter = fresh_class.new(a: 1, b: 2, c: 3)
1796
- expect(presenter.serializable_hash(only: %i[a b])).to eq(a: 1, b: 2)
1797
- expect(presenter.serializable_hash(only: %i[b c])).to eq(b: 2, c: 3)
1798
- end
1799
- end
1800
- end
1801
-
1802
- describe '#to_json' do
1803
- before do
1804
- fresh_class.expose :name
1805
- end
1806
-
1807
- it 'returns a json' do
1808
- expect(fresh_class.new(model).to_json).to eq(JSON.generate(attributes.slice(:name)))
1809
- end
1810
- end
1811
-
1812
- describe '#inspect' do
1813
- before do
1814
- fresh_class.class_eval do
1815
- expose :name, :email
1816
- end
1817
- end
1818
-
1819
- it 'does not serialize delegator or options' do
1820
- data = subject.inspect
1821
- expect(data).to include 'name='
1822
- expect(data).to include 'email='
1823
- expect(data).to_not include '@options'
1824
- expect(data).to_not include '@delegator'
1825
- end
1826
-
1827
- it 'returns a nil string when subject is nil' do
1828
- data = subject.class.new(nil).inspect
1829
- expect(data).to include 'nil'
1830
- end
1831
- end
1832
-
1833
- describe '#value_for' do
1834
- before do
1835
- fresh_class.class_eval do
1836
- expose :name, :email
1837
- expose :friends, using: self
1838
- expose :computed do |_, options|
1839
- options[:awesome]
1840
- end
1841
-
1842
- expose :birthday, format_with: :timestamp
1843
-
1844
- def timestamp(date)
1845
- date.strftime('%m/%d/%Y')
1846
- end
1847
-
1848
- expose :fantasies, format_with: ->(f) { f.reverse }
1849
- end
1850
- end
1851
-
1852
- it 'passes through bare expose attributes' do
1853
- expect(subject.value_for(:name)).to eq attributes[:name]
1854
- end
1855
-
1856
- it 'instantiates a representation if that is called for' do
1857
- rep = subject.value_for(:friends)
1858
- expect(rep.grep_v(fresh_class)).to be_empty
1859
- expect(rep.first.serializable_hash[:name]).to eq 'Friend 1'
1860
- expect(rep.last.serializable_hash[:name]).to eq 'Friend 2'
1861
- end
1862
-
1863
- context 'child representations' do
1864
- after { EntitySpec::FriendEntity.unexpose_all }
1865
-
1866
- it 'disables root key name for child representations' do
1867
- module EntitySpec
1868
- class FriendEntity < Grape::Entity
1869
- root 'friends', 'friend'
1870
- expose :name, :email
1871
- end
1872
- end
1873
-
1874
- fresh_class.class_eval do
1875
- expose :friends, using: EntitySpec::FriendEntity
1876
- end
1877
-
1878
- rep = subject.value_for(:friends)
1879
- expect(rep).to be_kind_of Array
1880
- expect(rep.grep_v(EntitySpec::FriendEntity)).to be_empty
1881
- expect(rep.first.serializable_hash[:name]).to eq 'Friend 1'
1882
- expect(rep.last.serializable_hash[:name]).to eq 'Friend 2'
1883
- end
1884
-
1885
- it 'passes through the proc which returns an array of objects with custom options(:using)' do
1886
- module EntitySpec
1887
- class FriendEntity < Grape::Entity
1888
- root 'friends', 'friend'
1889
- expose :name, :email
1890
- end
1891
- end
1892
-
1893
- fresh_class.class_eval do
1894
- expose :custom_friends, using: EntitySpec::FriendEntity do |user, _opts|
1895
- user.friends
1896
- end
1897
- end
1898
-
1899
- rep = subject.value_for(:custom_friends)
1900
- expect(rep).to be_kind_of Array
1901
- expect(rep.grep_v(EntitySpec::FriendEntity)).to be_empty
1902
- expect(rep.first.serializable_hash).to eq(name: 'Friend 1', email: 'friend1@example.com')
1903
- expect(rep.last.serializable_hash).to eq(name: 'Friend 2', email: 'friend2@example.com')
1904
- end
1905
-
1906
- it 'passes through the proc which returns single object with custom options(:using)' do
1907
- module EntitySpec
1908
- class FriendEntity < Grape::Entity
1909
- root 'friends', 'friend'
1910
- expose :name, :email
1911
- end
1912
- end
1913
-
1914
- fresh_class.class_eval do
1915
- expose :first_friend, using: EntitySpec::FriendEntity do |user, _opts|
1916
- user.friends.first
1917
- end
1918
- end
1919
-
1920
- rep = subject.value_for(:first_friend)
1921
- expect(rep).to be_kind_of EntitySpec::FriendEntity
1922
- expect(rep.serializable_hash).to eq(name: 'Friend 1', email: 'friend1@example.com')
1923
- end
1924
-
1925
- it 'passes through the proc which returns empty with custom options(:using)' do
1926
- module EntitySpec
1927
- class FriendEntity < Grape::Entity
1928
- root 'friends', 'friend'
1929
- expose :name, :email
1930
- end
1931
- end
1932
-
1933
- # rubocop:disable Lint/EmptyBlock
1934
- fresh_class.class_eval do
1935
- expose :first_friend, using: EntitySpec::FriendEntity do |_user, _opts|
1936
- end
1937
- end
1938
- # rubocop:enable Lint/EmptyBlock
1939
-
1940
- rep = subject.value_for(:first_friend)
1941
- expect(rep).to be_kind_of EntitySpec::FriendEntity
1942
- expect(rep.serializable_hash).to be_nil
1943
- end
1944
-
1945
- it 'passes through exposed entity with key and value attributes' do
1946
- module EntitySpec
1947
- class CharacteristicsEntity < Grape::Entity
1948
- root 'characteristics', 'characteristic'
1949
- expose :key, :value
1950
- end
1951
- end
1952
-
1953
- fresh_class.class_eval do
1954
- expose :characteristics, using: EntitySpec::CharacteristicsEntity
1955
- end
1956
-
1957
- rep = subject.value_for(:characteristics)
1958
- expect(rep).to be_kind_of Array
1959
- expect(rep.grep_v(EntitySpec::CharacteristicsEntity)).to be_empty
1960
- expect(rep.first.serializable_hash[:key]).to eq 'hair_color'
1961
- expect(rep.first.serializable_hash[:value]).to eq 'brown'
1962
- end
1963
-
1964
- it 'passes through custom options' do
1965
- module EntitySpec
1966
- class FriendEntity < Grape::Entity
1967
- root 'friends', 'friend'
1968
- expose :name
1969
- expose :email, if: { user_type: :admin }
1970
- end
1971
- end
1972
-
1973
- fresh_class.class_eval do
1974
- expose :friends, using: EntitySpec::FriendEntity
1975
- end
1976
-
1977
- rep = subject.value_for(:friends)
1978
- expect(rep).to be_kind_of Array
1979
- expect(rep.grep_v(EntitySpec::FriendEntity)).to be_empty
1980
- expect(rep.first.serializable_hash[:email]).to be_nil
1981
- expect(rep.last.serializable_hash[:email]).to be_nil
1982
-
1983
- rep = subject.value_for(:friends, Grape::Entity::Options.new(user_type: :admin))
1984
- expect(rep).to be_kind_of Array
1985
- expect(rep.grep_v(EntitySpec::FriendEntity)).to be_empty
1986
- expect(rep.first.serializable_hash[:email]).to eq 'friend1@example.com'
1987
- expect(rep.last.serializable_hash[:email]).to eq 'friend2@example.com'
1988
- end
1989
-
1990
- it 'ignores the :collection parameter in the source options' do
1991
- module EntitySpec
1992
- class FriendEntity < Grape::Entity
1993
- root 'friends', 'friend'
1994
- expose :name
1995
- expose :email, if: { collection: true }
1996
- end
1997
- end
1998
-
1999
- fresh_class.class_eval do
2000
- expose :friends, using: EntitySpec::FriendEntity
2001
- end
2002
-
2003
- rep = subject.value_for(:friends, Grape::Entity::Options.new(collection: false))
2004
- expect(rep).to be_kind_of Array
2005
- expect(rep.grep_v(EntitySpec::FriendEntity)).to be_empty
2006
- expect(rep.first.serializable_hash[:email]).to eq 'friend1@example.com'
2007
- expect(rep.last.serializable_hash[:email]).to eq 'friend2@example.com'
2008
- end
2009
- end
2010
-
2011
- it 'calls through to the proc if there is one' do
2012
- expect(subject.value_for(:computed, Grape::Entity::Options.new(awesome: 123))).to eq 123
2013
- end
2014
-
2015
- it 'returns a formatted value if format_with is passed' do
2016
- expect(subject.value_for(:birthday)).to eq '02/27/2012'
2017
- end
2018
-
2019
- it 'returns a formatted value if format_with is passed a lambda' do
2020
- expect(subject.value_for(:fantasies)).to eq ['Nessy', 'Double Rainbows', 'Unicorns']
2021
- end
2022
-
2023
- context 'delegate_attribute' do
2024
- module EntitySpec
2025
- class DelegatingEntity < Grape::Entity
2026
- root 'friends', 'friend'
2027
- expose :name
2028
- expose :email
2029
- expose :system
2030
-
2031
- private
2032
-
2033
- def name
2034
- 'cooler name'
2035
- end
2036
- end
2037
- end
2038
-
2039
- it 'tries instance methods on the entity first' do
2040
- friend = double('Friend', name: 'joe', email: 'joe@example.com')
2041
- rep = EntitySpec::DelegatingEntity.new(friend)
2042
- expect(rep.value_for(:name)).to eq 'cooler name'
2043
- expect(rep.value_for(:email)).to eq 'joe@example.com'
2044
-
2045
- another_friend = double('Friend', email: 'joe@example.com')
2046
- rep = EntitySpec::DelegatingEntity.new(another_friend)
2047
- expect(rep.value_for(:name)).to eq 'cooler name'
2048
- end
2049
-
2050
- it 'does not delegate Kernel methods' do
2051
- foo = double 'Foo', system: 'System'
2052
- rep = EntitySpec::DelegatingEntity.new foo
2053
- expect(rep.value_for(:system)).to eq 'System'
2054
- end
2055
-
2056
- module EntitySpec
2057
- class DerivedEntity < DelegatingEntity
2058
- end
2059
- end
2060
-
2061
- it 'derived entity get methods from base entity' do
2062
- foo = double 'Foo', name: 'joe'
2063
- rep = EntitySpec::DerivedEntity.new foo
2064
- expect(rep.value_for(:name)).to eq 'cooler name'
2065
- end
2066
- end
2067
-
2068
- context 'using' do
2069
- before do
2070
- module EntitySpec
2071
- class UserEntity < Grape::Entity
2072
- expose :name, :email
2073
- end
2074
- end
2075
- end
2076
- it 'string' do
2077
- fresh_class.class_eval do
2078
- expose :friends, using: 'EntitySpec::UserEntity'
2079
- end
2080
-
2081
- rep = subject.value_for(:friends)
2082
- expect(rep).to be_kind_of Array
2083
- expect(rep.size).to eq 2
2084
- expect(rep.all?(EntitySpec::UserEntity)).to be true
2085
- end
2086
-
2087
- it 'class' do
2088
- fresh_class.class_eval do
2089
- expose :friends, using: EntitySpec::UserEntity
2090
- end
2091
-
2092
- rep = subject.value_for(:friends)
2093
- expect(rep).to be_kind_of Array
2094
- expect(rep.size).to eq 2
2095
- expect(rep.all?(EntitySpec::UserEntity)).to be true
2096
- end
2097
- end
2098
- end
2099
-
2100
- describe '.documentation' do
2101
- it 'returns an empty hash is no documentation is provided' do
2102
- fresh_class.expose :name
2103
-
2104
- expect(subject.documentation).to eq({})
2105
- end
2106
-
2107
- it 'returns each defined documentation hash' do
2108
- doc = { type: 'foo', desc: 'bar' }
2109
- fresh_class.expose :name, documentation: doc
2110
- fresh_class.expose :email, documentation: doc
2111
- fresh_class.expose :birthday
2112
-
2113
- expect(subject.documentation).to eq(name: doc, email: doc)
2114
- end
2115
-
2116
- it 'returns each defined documentation hash with :as param considering' do
2117
- doc = { type: 'foo', desc: 'bar' }
2118
- fresh_class.expose :name, documentation: doc, as: :label
2119
- fresh_class.expose :email, documentation: doc
2120
- fresh_class.expose :birthday
2121
-
2122
- expect(subject.documentation).to eq(label: doc, email: doc)
2123
- end
2124
-
2125
- it 'resets memoization when exposing additional attributes' do
2126
- fresh_class.expose :x, documentation: { desc: 'just x' }
2127
- expect(fresh_class.instance_variable_get(:@documentation)).to be_nil
2128
- doc1 = fresh_class.documentation
2129
- expect(fresh_class.instance_variable_get(:@documentation)).not_to be_nil
2130
- fresh_class.expose :y, documentation: { desc: 'just y' }
2131
- expect(fresh_class.instance_variable_get(:@documentation)).to be_nil
2132
- doc2 = fresh_class.documentation
2133
- expect(doc1).to eq(x: { desc: 'just x' })
2134
- expect(doc2).to eq(x: { desc: 'just x' }, y: { desc: 'just y' })
2135
- end
2136
-
2137
- context 'inherited documentation' do
2138
- it 'returns documentation from ancestor' do
2139
- doc = { type: 'foo', desc: 'bar' }
2140
- fresh_class.expose :name, documentation: doc
2141
- child_class = Class.new(fresh_class)
2142
- child_class.expose :email, documentation: doc
2143
-
2144
- expect(fresh_class.documentation).to eq(name: doc)
2145
- expect(child_class.documentation).to eq(name: doc, email: doc)
2146
- end
2147
-
2148
- it 'obeys unexposed attributes in subclass' do
2149
- doc = { type: 'foo', desc: 'bar' }
2150
- fresh_class.expose :name, documentation: doc
2151
- fresh_class.expose :email, documentation: doc
2152
- child_class = Class.new(fresh_class)
2153
- child_class.unexpose :email
2154
-
2155
- expect(fresh_class.documentation).to eq(name: doc, email: doc)
2156
- expect(child_class.documentation).to eq(name: doc)
2157
- end
2158
-
2159
- it 'obeys re-exposed attributes in subclass' do
2160
- doc = { type: 'foo', desc: 'bar' }
2161
- fresh_class.expose :name, documentation: doc
2162
- fresh_class.expose :email, documentation: doc
2163
-
2164
- child_class = Class.new(fresh_class)
2165
- child_class.unexpose :email
2166
-
2167
- nephew_class = Class.new(child_class)
2168
- new_doc = { type: 'todler', descr: '???' }
2169
- nephew_class.expose :email, documentation: new_doc
2170
-
2171
- expect(fresh_class.documentation).to eq(name: doc, email: doc)
2172
- expect(child_class.documentation).to eq(name: doc)
2173
- expect(nephew_class.documentation).to eq(name: doc, email: new_doc)
2174
- end
2175
-
2176
- it 'includes only root exposures' do
2177
- fresh_class.expose :name, documentation: { desc: 'foo' }
2178
- fresh_class.expose :nesting do
2179
- fresh_class.expose :smth, documentation: { desc: 'should not be seen' }
2180
- end
2181
- expect(fresh_class.documentation).to eq(name: { desc: 'foo' })
2182
- end
2183
- end
2184
- end
2185
-
2186
- describe '::DSL' do
2187
- subject { Class.new }
2188
-
2189
- it 'creates an Entity class when called' do
2190
- expect(subject).not_to be_const_defined :Entity
2191
- subject.send(:include, Grape::Entity::DSL)
2192
- expect(subject).to be_const_defined :Entity
2193
- end
2194
-
2195
- context 'pre-mixed' do
2196
- before { subject.send(:include, Grape::Entity::DSL) }
2197
-
2198
- it 'is able to define entity traits through DSL' do
2199
- subject.entity do
2200
- expose :name
2201
- end
2202
-
2203
- expect(subject.entity_class.root_exposures).not_to be_empty
2204
- end
2205
-
2206
- it 'is able to expose straight from the class' do
2207
- subject.entity :name, :email
2208
- expect(subject.entity_class.root_exposures.size).to eq 2
2209
- end
2210
-
2211
- it 'is able to mix field and advanced.root_exposures' do
2212
- subject.entity :name, :email do
2213
- expose :third
2214
- end
2215
- expect(subject.entity_class.root_exposures.size).to eq 3
2216
- end
2217
-
2218
- context 'instance' do
2219
- let(:instance) { subject.new }
2220
-
2221
- describe '#entity' do
2222
- it 'is an instance of the entity class' do
2223
- expect(instance.entity).to be_kind_of(subject.entity_class)
2224
- end
2225
-
2226
- it 'has an object of itself' do
2227
- expect(instance.entity.object).to eq instance
2228
- end
2229
-
2230
- it 'instantiates with options if provided' do
2231
- expect(instance.entity(awesome: true).options).to eq(awesome: true)
2232
- end
2233
- end
2234
- end
2235
- end
2236
- end
2237
- end
2238
- end