grape-entity 1.0.1 → 1.0.4

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