grape-entity 0.4.8 → 0.10.2

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.
Files changed (55) hide show
  1. checksums.yaml +5 -5
  2. data/.coveralls.yml +1 -0
  3. data/.github/dependabot.yml +20 -0
  4. data/.github/workflows/ci.yml +41 -0
  5. data/.gitignore +5 -1
  6. data/.rspec +2 -1
  7. data/.rubocop.yml +85 -2
  8. data/.rubocop_todo.yml +41 -33
  9. data/CHANGELOG.md +243 -37
  10. data/CONTRIBUTING.md +1 -1
  11. data/Dangerfile +3 -0
  12. data/Gemfile +11 -7
  13. data/Guardfile +4 -2
  14. data/LICENSE +1 -1
  15. data/README.md +272 -19
  16. data/Rakefile +9 -11
  17. data/UPGRADING.md +35 -0
  18. data/bench/serializing.rb +7 -0
  19. data/grape-entity.gemspec +13 -8
  20. data/lib/grape-entity.rb +2 -0
  21. data/lib/grape_entity/condition/base.rb +37 -0
  22. data/lib/grape_entity/condition/block_condition.rb +23 -0
  23. data/lib/grape_entity/condition/hash_condition.rb +27 -0
  24. data/lib/grape_entity/condition/symbol_condition.rb +23 -0
  25. data/lib/grape_entity/condition.rb +35 -0
  26. data/lib/grape_entity/delegator/base.rb +7 -0
  27. data/lib/grape_entity/delegator/fetchable_object.rb +2 -0
  28. data/lib/grape_entity/delegator/hash_object.rb +4 -2
  29. data/lib/grape_entity/delegator/openstruct_object.rb +2 -0
  30. data/lib/grape_entity/delegator/plain_object.rb +2 -0
  31. data/lib/grape_entity/delegator.rb +14 -9
  32. data/lib/grape_entity/deprecated.rb +13 -0
  33. data/lib/grape_entity/entity.rb +192 -258
  34. data/lib/grape_entity/exposure/base.rb +138 -0
  35. data/lib/grape_entity/exposure/block_exposure.rb +31 -0
  36. data/lib/grape_entity/exposure/delegator_exposure.rb +13 -0
  37. data/lib/grape_entity/exposure/formatter_block_exposure.rb +27 -0
  38. data/lib/grape_entity/exposure/formatter_exposure.rb +32 -0
  39. data/lib/grape_entity/exposure/nesting_exposure/nested_exposures.rb +83 -0
  40. data/lib/grape_entity/exposure/nesting_exposure/output_builder.rb +66 -0
  41. data/lib/grape_entity/exposure/nesting_exposure.rb +133 -0
  42. data/lib/grape_entity/exposure/represent_exposure.rb +50 -0
  43. data/lib/grape_entity/exposure.rb +105 -0
  44. data/lib/grape_entity/options.rb +132 -0
  45. data/lib/grape_entity/version.rb +3 -1
  46. data/lib/grape_entity.rb +9 -2
  47. data/spec/grape_entity/entity_spec.rb +903 -184
  48. data/spec/grape_entity/exposure/nesting_exposure/nested_exposures_spec.rb +58 -0
  49. data/spec/grape_entity/exposure/represent_exposure_spec.rb +32 -0
  50. data/spec/grape_entity/exposure_spec.rb +102 -0
  51. data/spec/grape_entity/hash_spec.rb +91 -0
  52. data/spec/grape_entity/options_spec.rb +66 -0
  53. data/spec/spec_helper.rb +21 -2
  54. metadata +91 -18
  55. data/.travis.yml +0 -19
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'spec_helper'
2
4
  require 'ostruct'
3
5
 
@@ -11,12 +13,12 @@ describe Grape::Entity do
11
13
  context 'multiple attributes' do
12
14
  it 'is able to add multiple exposed attributes with a single call' do
13
15
  subject.expose :name, :email, :location
14
- expect(subject.exposures.size).to eq 3
16
+ expect(subject.root_exposures.size).to eq 3
15
17
  end
16
18
 
17
- it 'sets the same options for all exposures passed' do
19
+ it 'sets the same options for all.root_exposures passed' do
18
20
  subject.expose :name, :email, :location, documentation: true
19
- subject.exposures.values.each { |v| expect(v).to eq(documentation: true) }
21
+ subject.root_exposures.each { |v| expect(v.documentation).to eq true }
20
22
  end
21
23
  end
22
24
 
@@ -27,7 +29,9 @@ describe Grape::Entity do
27
29
  end
28
30
 
29
31
  it 'makes sure that :format_with as a proc cannot be used with a block' do
30
- expect { subject.expose :name, format_with: proc {} {} }.to raise_error ArgumentError
32
+ # rubocop:disable Style/BlockDelimiters
33
+ expect { subject.expose :name, format_with: proc {} do p 'hi' end }.to raise_error ArgumentError
34
+ # rubocop:enable Style/BlockDelimiters
31
35
  end
32
36
 
33
37
  it 'makes sure unknown options are not silently ignored' do
@@ -35,6 +39,303 @@ describe Grape::Entity do
35
39
  end
36
40
  end
37
41
 
42
+ context 'with a :merge option' do
43
+ let(:nested_hash) do
44
+ { something: { like_nested_hash: true }, special: { like_nested_hash: '12' } }
45
+ end
46
+
47
+ it 'merges an exposure to the root' do
48
+ subject.expose(:something, merge: true)
49
+ expect(subject.represent(nested_hash).serializable_hash).to eq(nested_hash[:something])
50
+ end
51
+
52
+ it 'allows to solve collisions providing a lambda to a :merge option' do
53
+ subject.expose(:something, merge: true)
54
+ subject.expose(:special, merge: ->(_, v1, v2) { v1 && v2 ? 'brand new val' : v2 })
55
+ expect(subject.represent(nested_hash).serializable_hash).to eq(like_nested_hash: 'brand new val')
56
+ end
57
+
58
+ context 'and nested object is nil' do
59
+ let(:nested_hash) do
60
+ { something: nil, special: { like_nested_hash: '12' } }
61
+ end
62
+
63
+ it 'adds nothing to output' do
64
+ subject.expose(:something, merge: true)
65
+ subject.expose(:special)
66
+ expect(subject.represent(nested_hash).serializable_hash).to eq(special: { like_nested_hash: '12' })
67
+ end
68
+ end
69
+ end
70
+
71
+ context 'with :expose_nil option' do
72
+ let(:a) { nil }
73
+ let(:b) { nil }
74
+ let(:c) { 'value' }
75
+
76
+ context 'when model is a PORO' do
77
+ let(:model) { Model.new(a, b, c) }
78
+
79
+ before do
80
+ stub_const 'Model', Class.new
81
+ Model.class_eval do
82
+ attr_accessor :a, :b, :c
83
+
84
+ def initialize(a, b, c)
85
+ @a = a
86
+ @b = b
87
+ @c = c
88
+ end
89
+ end
90
+ end
91
+
92
+ context 'when expose_nil option is not provided' do
93
+ it 'exposes nil attributes' do
94
+ subject.expose(:a)
95
+ subject.expose(:b)
96
+ subject.expose(:c)
97
+ expect(subject.represent(model).serializable_hash).to eq(a: nil, b: nil, c: 'value')
98
+ end
99
+ end
100
+
101
+ context 'when expose_nil option is true' do
102
+ it 'exposes nil attributes' do
103
+ subject.expose(:a, expose_nil: true)
104
+ subject.expose(:b, expose_nil: true)
105
+ subject.expose(:c)
106
+ expect(subject.represent(model).serializable_hash).to eq(a: nil, b: nil, c: 'value')
107
+ end
108
+ end
109
+
110
+ context 'when expose_nil option is false' do
111
+ it 'does not expose nil attributes' do
112
+ subject.expose(:a, expose_nil: false)
113
+ subject.expose(:b, expose_nil: false)
114
+ subject.expose(:c)
115
+ expect(subject.represent(model).serializable_hash).to eq(c: 'value')
116
+ end
117
+
118
+ it 'is only applied per attribute' do
119
+ subject.expose(:a, expose_nil: false)
120
+ subject.expose(:b)
121
+ subject.expose(:c)
122
+ expect(subject.represent(model).serializable_hash).to eq(b: nil, c: 'value')
123
+ end
124
+
125
+ it 'raises an error when applied to multiple attribute exposures' do
126
+ expect { subject.expose(:a, :b, :c, expose_nil: false) }.to raise_error ArgumentError
127
+ end
128
+ end
129
+
130
+ context 'when expose_nil option is false and block passed' do
131
+ it 'does not expose if block returns nil' do
132
+ subject.expose(:a, expose_nil: false) do |_obj, _options|
133
+ nil
134
+ end
135
+ subject.expose(:b)
136
+ subject.expose(:c)
137
+ expect(subject.represent(model).serializable_hash).to eq(b: nil, c: 'value')
138
+ end
139
+
140
+ it 'exposes is block returns a value' do
141
+ subject.expose(:a, expose_nil: false) do |_obj, _options|
142
+ 100
143
+ end
144
+ subject.expose(:b)
145
+ subject.expose(:c)
146
+ expect(subject.represent(model).serializable_hash).to eq(a: 100, b: nil, c: 'value')
147
+ end
148
+ end
149
+ end
150
+
151
+ context 'when model is a hash' do
152
+ let(:model) { { a: a, b: b, c: c } }
153
+
154
+ context 'when expose_nil option is not provided' do
155
+ it 'exposes nil attributes' do
156
+ subject.expose(:a)
157
+ subject.expose(:b)
158
+ subject.expose(:c)
159
+ expect(subject.represent(model).serializable_hash).to eq(a: nil, b: nil, c: 'value')
160
+ end
161
+ end
162
+
163
+ context 'when expose_nil option is true' do
164
+ it 'exposes nil attributes' do
165
+ subject.expose(:a, expose_nil: true)
166
+ subject.expose(:b, expose_nil: true)
167
+ subject.expose(:c)
168
+ expect(subject.represent(model).serializable_hash).to eq(a: nil, b: nil, c: 'value')
169
+ end
170
+ end
171
+
172
+ context 'when expose_nil option is false' do
173
+ it 'does not expose nil attributes' do
174
+ subject.expose(:a, expose_nil: false)
175
+ subject.expose(:b, expose_nil: false)
176
+ subject.expose(:c)
177
+ expect(subject.represent(model).serializable_hash).to eq(c: 'value')
178
+ end
179
+
180
+ it 'is only applied per attribute' do
181
+ subject.expose(:a, expose_nil: false)
182
+ subject.expose(:b)
183
+ subject.expose(:c)
184
+ expect(subject.represent(model).serializable_hash).to eq(b: nil, c: 'value')
185
+ end
186
+
187
+ it 'raises an error when applied to multiple attribute exposures' do
188
+ expect { subject.expose(:a, :b, :c, expose_nil: false) }.to raise_error ArgumentError
189
+ end
190
+ end
191
+ end
192
+
193
+ context 'with nested structures' do
194
+ let(:model) { { a: a, b: b, c: { d: nil, e: nil, f: { g: nil, h: nil } } } }
195
+
196
+ context 'when expose_nil option is false' do
197
+ it 'does not expose nil attributes' do
198
+ subject.expose(:a, expose_nil: false)
199
+ subject.expose(:b)
200
+ subject.expose(:c) do
201
+ subject.expose(:d, expose_nil: false)
202
+ subject.expose(:e)
203
+ subject.expose(:f) do
204
+ subject.expose(:g, expose_nil: false)
205
+ subject.expose(:h)
206
+ end
207
+ end
208
+
209
+ expect(subject.represent(model).serializable_hash).to eq(b: nil, c: { e: nil, f: { h: nil } })
210
+ end
211
+ end
212
+ end
213
+ end
214
+
215
+ context 'with :default option' do
216
+ let(:a) { nil }
217
+ let(:b) { nil }
218
+ let(:c) { 'value' }
219
+
220
+ context 'when model is a PORO' do
221
+ let(:model) { Model.new(a, b, c) }
222
+
223
+ before do
224
+ stub_const 'Model', Class.new
225
+ Model.class_eval do
226
+ attr_accessor :a, :b, :c
227
+
228
+ def initialize(a, b, c)
229
+ @a = a
230
+ @b = b
231
+ @c = c
232
+ end
233
+ end
234
+ end
235
+
236
+ context 'when default option is not provided' do
237
+ it 'exposes attributes values' do
238
+ subject.expose(:a)
239
+ subject.expose(:b)
240
+ subject.expose(:c)
241
+ expect(subject.represent(model).serializable_hash).to eq(a: nil, b: nil, c: 'value')
242
+ end
243
+ end
244
+
245
+ context 'when default option is set' do
246
+ it 'exposes default values for attributes' do
247
+ subject.expose(:a, default: 'a')
248
+ subject.expose(:b, default: 'b')
249
+ subject.expose(:c, default: 'c')
250
+ expect(subject.represent(model).serializable_hash).to eq(a: 'a', b: 'b', c: 'value')
251
+ end
252
+ end
253
+
254
+ context 'when default option is set and block passed' do
255
+ it 'return default value if block returns nil' do
256
+ subject.expose(:a, default: 'a') do |_obj, _options|
257
+ nil
258
+ end
259
+ subject.expose(:b)
260
+ subject.expose(:c)
261
+ expect(subject.represent(model).serializable_hash).to eq(a: 'a', b: nil, c: 'value')
262
+ end
263
+
264
+ it 'return value from block if block returns a value' do
265
+ subject.expose(:a, default: 'a') do |_obj, _options|
266
+ 100
267
+ end
268
+ subject.expose(:b)
269
+ subject.expose(:c)
270
+ expect(subject.represent(model).serializable_hash).to eq(a: 100, b: nil, c: 'value')
271
+ end
272
+ end
273
+ end
274
+
275
+ context 'when model is a hash' do
276
+ let(:model) { { a: a, b: b, c: c } }
277
+
278
+ context 'when expose_nil option is not provided' do
279
+ it 'exposes nil attributes' do
280
+ subject.expose(:a)
281
+ subject.expose(:b)
282
+ subject.expose(:c)
283
+ expect(subject.represent(model).serializable_hash).to eq(a: nil, b: nil, c: 'value')
284
+ end
285
+ end
286
+
287
+ context 'when expose_nil option is true' do
288
+ it 'exposes nil attributes' do
289
+ subject.expose(:a, expose_nil: true)
290
+ subject.expose(:b, expose_nil: true)
291
+ subject.expose(:c)
292
+ expect(subject.represent(model).serializable_hash).to eq(a: nil, b: nil, c: 'value')
293
+ end
294
+ end
295
+
296
+ context 'when expose_nil option is false' do
297
+ it 'does not expose nil attributes' do
298
+ subject.expose(:a, expose_nil: false)
299
+ subject.expose(:b, expose_nil: false)
300
+ subject.expose(:c)
301
+ expect(subject.represent(model).serializable_hash).to eq(c: 'value')
302
+ end
303
+
304
+ it 'is only applied per attribute' do
305
+ subject.expose(:a, expose_nil: false)
306
+ subject.expose(:b)
307
+ subject.expose(:c)
308
+ expect(subject.represent(model).serializable_hash).to eq(b: nil, c: 'value')
309
+ end
310
+
311
+ it 'raises an error when applied to multiple attribute exposures' do
312
+ expect { subject.expose(:a, :b, :c, expose_nil: false) }.to raise_error ArgumentError
313
+ end
314
+ end
315
+ end
316
+
317
+ context 'with nested structures' do
318
+ let(:model) { { a: a, b: b, c: { d: nil, e: nil, f: { g: nil, h: nil } } } }
319
+
320
+ context 'when expose_nil option is false' do
321
+ it 'does not expose nil attributes' do
322
+ subject.expose(:a, expose_nil: false)
323
+ subject.expose(:b)
324
+ subject.expose(:c) do
325
+ subject.expose(:d, expose_nil: false)
326
+ subject.expose(:e)
327
+ subject.expose(:f) do
328
+ subject.expose(:g, expose_nil: false)
329
+ subject.expose(:h)
330
+ end
331
+ end
332
+
333
+ expect(subject.represent(model).serializable_hash).to eq(b: nil, c: { e: nil, f: { h: nil } })
334
+ end
335
+ end
336
+ end
337
+ end
338
+
38
339
  context 'with a block' do
39
340
  it 'errors out if called with multiple attributes' do
40
341
  expect { subject.expose(:name, :email) { true } }.to raise_error ArgumentError
@@ -61,10 +362,10 @@ describe Grape::Entity do
61
362
  end
62
363
 
63
364
  object = EntitySpec::SomeObject1.new
64
- value = subject.represent(object).send(:value_for, :bogus)
365
+ value = subject.represent(object).value_for(:bogus)
65
366
  expect(value).to be_instance_of EntitySpec::BogusEntity
66
367
 
67
- prop1 = value.send(:value_for, :prop1)
368
+ prop1 = value.value_for(:prop1)
68
369
  expect(prop1).to eq 'MODIFIED 2'
69
370
  end
70
371
 
@@ -72,12 +373,76 @@ describe Grape::Entity do
72
373
  it 'sets the :proc option in the exposure options' do
73
374
  block = ->(_) { true }
74
375
  subject.expose :name, using: 'Awesome', &block
75
- expect(subject.exposures[:name]).to eq(proc: block, using: 'Awesome')
376
+ exposure = subject.find_exposure(:name)
377
+ expect(exposure.subexposure.block).to eq(block)
378
+ expect(exposure.using_class_name).to eq('Awesome')
76
379
  end
77
380
 
78
381
  it 'references an instance of the entity without any options' do
79
382
  subject.expose(:size) { |_| self }
80
- expect(subject.represent({}).send(:value_for, :size)).to be_an_instance_of fresh_class
383
+ expect(subject.represent({}).value_for(:size)).to be_an_instance_of fresh_class
384
+ end
385
+ end
386
+
387
+ describe 'blocks' do
388
+ class SomeObject
389
+ def method_without_args
390
+ 'result'
391
+ end
392
+
393
+ def raises_argument_error
394
+ raise ArgumentError, 'something different'
395
+ end
396
+ end
397
+
398
+ describe 'with block passed in' do
399
+ specify do
400
+ subject.expose :that_method_without_args do |object|
401
+ object.method_without_args
402
+ end
403
+
404
+ object = SomeObject.new
405
+
406
+ value = subject.represent(object).value_for(:that_method_without_args)
407
+ expect(value).to eq('result')
408
+ end
409
+
410
+ it 'does not suppress ArgumentError' do
411
+ subject.expose :raises_argument_error do |object|
412
+ object.raises_argument_error
413
+ end
414
+
415
+ object = SomeObject.new
416
+ expect do
417
+ subject.represent(object).value_for(:raises_argument_error)
418
+ end.to raise_error(ArgumentError, 'something different')
419
+ end
420
+ end
421
+
422
+ context 'with block passed in via &' do
423
+ if RUBY_VERSION.start_with?('3')
424
+ specify do
425
+ subject.expose :that_method_without_args, &:method_without_args
426
+ subject.expose :method_without_args, as: :that_method_without_args_again
427
+
428
+ object = SomeObject.new
429
+ expect do
430
+ subject.represent(object).value_for(:that_method_without_args)
431
+ end.to raise_error Grape::Entity::Deprecated
432
+
433
+ value2 = subject.represent(object).value_for(:that_method_without_args_again)
434
+ expect(value2).to eq('result')
435
+ end
436
+ else
437
+ specify do
438
+ subject.expose :that_method_without_args_again, &:method_without_args
439
+
440
+ object = SomeObject.new
441
+
442
+ value2 = subject.represent(object).value_for(:that_method_without_args_again)
443
+ expect(value2).to eq('result')
444
+ end
445
+ end
81
446
  end
82
447
  end
83
448
 
@@ -90,33 +455,38 @@ describe Grape::Entity do
90
455
  subject.expose :another_nested, using: 'Awesome'
91
456
  end
92
457
 
93
- expect(subject.exposures).to eq(
94
- awesome: {},
95
- awesome__nested: { nested: true },
96
- awesome__nested__moar_nested: { as: 'weee', nested: true },
97
- awesome__another_nested: { using: 'Awesome', nested: true }
98
- )
458
+ awesome = subject.find_exposure(:awesome)
459
+ nested = awesome.find_nested_exposure(:nested)
460
+ another_nested = awesome.find_nested_exposure(:another_nested)
461
+ moar_nested = nested.find_nested_exposure(:moar_nested)
462
+
463
+ expect(awesome).to be_nesting
464
+ expect(nested).to_not be_nil
465
+ expect(another_nested).to_not be_nil
466
+ expect(another_nested.using_class_name).to eq('Awesome')
467
+ expect(moar_nested).to_not be_nil
468
+ expect(moar_nested.key(subject)).to eq(:weee)
99
469
  end
100
470
 
101
- it 'represents the exposure as a hash of its nested exposures' do
471
+ it 'represents the exposure as a hash of its nested.root_exposures' do
102
472
  subject.expose :awesome do
103
473
  subject.expose(:nested) { |_| 'value' }
104
474
  subject.expose(:another_nested) { |_| 'value' }
105
475
  end
106
476
 
107
- expect(subject.represent({}).send(:value_for, :awesome)).to eq(
477
+ expect(subject.represent({}).value_for(:awesome)).to eq(
108
478
  nested: 'value',
109
479
  another_nested: 'value'
110
480
  )
111
481
  end
112
482
 
113
- it 'does not represent nested exposures whose conditions are not met' do
483
+ it 'does not represent nested.root_exposures whose conditions are not met' do
114
484
  subject.expose :awesome do
115
485
  subject.expose(:condition_met, if: ->(_, _) { true }) { |_| 'value' }
116
486
  subject.expose(:condition_not_met, if: ->(_, _) { false }) { |_| 'value' }
117
487
  end
118
488
 
119
- expect(subject.represent({}).send(:value_for, :awesome)).to eq(condition_met: 'value')
489
+ expect(subject.represent({}).value_for(:awesome)).to eq(condition_met: 'value')
120
490
  end
121
491
 
122
492
  it 'does not represent attributes, declared inside nested exposure, outside of it' do
@@ -139,7 +509,7 @@ describe Grape::Entity do
139
509
  )
140
510
  end
141
511
 
142
- it 'complex nested attributes' do
512
+ it 'merges complex nested attributes' do
143
513
  class ClassRoom < Grape::Entity
144
514
  expose(:parents, using: 'Parent') { |_| [{}, {}] }
145
515
  end
@@ -181,7 +551,44 @@ describe Grape::Entity do
181
551
  )
182
552
  end
183
553
 
184
- it 'is safe if its nested exposures are safe' do
554
+ it 'merges results of deeply nested double.root_exposures inside of nesting exposure' do
555
+ entity = Class.new(Grape::Entity) do
556
+ expose :data do
557
+ expose :something do
558
+ expose(:x) { |_| 'x' }
559
+ end
560
+ expose :something do
561
+ expose(:y) { |_| 'y' }
562
+ end
563
+ end
564
+ end
565
+ expect(entity.represent({}).serializable_hash).to eq(
566
+ data: {
567
+ something: {
568
+ x: 'x',
569
+ y: 'y'
570
+ }
571
+ }
572
+ )
573
+ end
574
+
575
+ it 'serializes deeply nested presenter exposures' do
576
+ e = Class.new(Grape::Entity) do
577
+ expose :f
578
+ end
579
+ subject.expose :a do
580
+ subject.expose :b do
581
+ subject.expose :c do
582
+ subject.expose :lol, using: e
583
+ end
584
+ end
585
+ end
586
+ expect(subject.represent(lol: { f: 123 }).serializable_hash).to eq(
587
+ a: { b: { c: { lol: { f: 123 } } } }
588
+ )
589
+ end
590
+
591
+ it 'is safe if its nested.root_exposures are safe' do
185
592
  subject.with_options safe: true do
186
593
  subject.expose :awesome do
187
594
  subject.expose(:nested) { |_| 'value' }
@@ -199,34 +606,75 @@ describe Grape::Entity do
199
606
  }
200
607
  )
201
608
  end
609
+ it 'merges attriutes if :merge option is passed' do
610
+ user_entity = Class.new(Grape::Entity)
611
+ admin_entity = Class.new(Grape::Entity)
612
+ user_entity.expose(:id, :name)
613
+ admin_entity.expose(:id, :name)
614
+
615
+ subject.expose(:profiles) do
616
+ subject.expose(:users, merge: true, using: user_entity)
617
+ subject.expose(:admins, merge: true, using: admin_entity)
618
+ end
619
+
620
+ subject.expose :awesome do
621
+ subject.expose(:nested, merge: true) { |_| { just_a_key: 'value' } }
622
+ subject.expose(:another_nested, merge: true) { |_| { just_another_key: 'value' } }
623
+ end
624
+
625
+ additional_hash = { users: [{ id: 1, name: 'John' }, { id: 2, name: 'Jay' }],
626
+ admins: [{ id: 3, name: 'Jack' }, { id: 4, name: 'James' }] }
627
+ expect(subject.represent(additional_hash).serializable_hash).to eq(
628
+ profiles: additional_hash[:users] + additional_hash[:admins],
629
+ awesome: { just_a_key: 'value', just_another_key: 'value' }
630
+ )
631
+ end
202
632
  end
203
633
  end
204
634
 
205
- context 'inherited exposures' do
206
- it 'returns exposures from an ancestor' do
635
+ context 'inherited.root_exposures' do
636
+ it 'returns.root_exposures from an ancestor' do
207
637
  subject.expose :name, :email
208
638
  child_class = Class.new(subject)
209
639
 
210
- expect(child_class.exposures).to eq(subject.exposures)
640
+ expect(child_class.root_exposures).to eq(subject.root_exposures)
211
641
  end
212
642
 
213
- it 'returns exposures from multiple ancestor' do
643
+ it 'returns.root_exposures from multiple ancestor' do
214
644
  subject.expose :name, :email
215
645
  parent_class = Class.new(subject)
216
646
  child_class = Class.new(parent_class)
217
647
 
218
- expect(child_class.exposures).to eq(subject.exposures)
648
+ expect(child_class.root_exposures).to eq(subject.root_exposures)
219
649
  end
220
650
 
221
- it 'returns descendant exposures as a priority' do
651
+ it 'returns descendant.root_exposures as a priority' do
222
652
  subject.expose :name, :email
223
653
  child_class = Class.new(subject)
224
654
  child_class.expose :name do |_|
225
655
  'foo'
226
656
  end
227
657
 
228
- expect(subject.exposures[:name]).not_to have_key :proc
229
- expect(child_class.exposures[:name]).to have_key :proc
658
+ expect(subject.represent({ name: 'bar' }, serializable: true)).to eq(email: nil, name: 'bar')
659
+ expect(child_class.represent({ name: 'bar' }, serializable: true)).to eq(email: nil, name: 'foo')
660
+ end
661
+
662
+ it 'not overrides exposure by default' do
663
+ subject.expose :name
664
+ child_class = Class.new(subject)
665
+ child_class.expose :name, as: :child_name
666
+
667
+ expect(subject.represent({ name: 'bar' }, serializable: true)).to eq(name: 'bar')
668
+ expect(child_class.represent({ name: 'bar' }, serializable: true)).to eq(name: 'bar', child_name: 'bar')
669
+ end
670
+
671
+ it 'overrides parent class exposure when option is specified' do
672
+ subject.expose :name
673
+ child_class = Class.new(subject)
674
+ child_class.expose :name, as: :child_name, override: true
675
+
676
+ expect(subject.represent({ name: 'bar' }, serializable: true)).to eq(name: 'bar')
677
+ expect(child_class.represent({ name: 'bar' }, serializable: true)).to eq(child_name: 'bar')
230
678
  end
231
679
  end
232
680
 
@@ -257,46 +705,46 @@ describe Grape::Entity do
257
705
 
258
706
  subject.expose :birthday, format_with: :timestamp
259
707
 
260
- model = { birthday: Time.gm(2012, 2, 27) }
708
+ model = { birthday: Time.gm(2012, 2, 27) }
261
709
  expect(subject.new(double(model)).as_json[:birthday]).to eq '02/27/2012'
262
710
  end
263
711
 
264
712
  it 'formats an exposure with a :format_with lambda that returns a value from the entity instance' do
265
713
  object = {}
266
714
 
267
- subject.expose(:size, format_with: ->(_value) { self.object.class.to_s })
268
- expect(subject.represent(object).send(:value_for, :size)).to eq object.class.to_s
715
+ subject.expose(:size, format_with: ->(_value) { object.class.to_s })
716
+ expect(subject.represent(object).value_for(:size)).to eq object.class.to_s
269
717
  end
270
718
 
271
719
  it 'formats an exposure with a :format_with symbol that returns a value from the entity instance' do
272
720
  subject.format_with :size_formatter do |_date|
273
- self.object.class.to_s
721
+ object.class.to_s
274
722
  end
275
723
 
276
724
  object = {}
277
725
 
278
726
  subject.expose(:size, format_with: :size_formatter)
279
- expect(subject.represent(object).send(:value_for, :size)).to eq object.class.to_s
727
+ expect(subject.represent(object).value_for(:size)).to eq object.class.to_s
280
728
  end
281
729
 
282
730
  it 'works global on Grape::Entity' do
283
731
  Grape::Entity.format_with :size_formatter do |_date|
284
- self.object.class.to_s
732
+ object.class.to_s
285
733
  end
286
734
  object = {}
287
735
 
288
736
  subject.expose(:size, format_with: :size_formatter)
289
- expect(subject.represent(object).send(:value_for, :size)).to eq object.class.to_s
737
+ expect(subject.represent(object).value_for(:size)).to eq object.class.to_s
290
738
  end
291
739
  end
292
740
 
293
741
  it 'works global on Grape::Entity' do
294
742
  Grape::Entity.expose :a
295
743
  object = { a: 11, b: 22 }
296
- expect(Grape::Entity.represent(object).send(:value_for, :a)).to eq 11
744
+ expect(Grape::Entity.represent(object).value_for(:a)).to eq 11
297
745
  subject.expose :b
298
- expect(subject.represent(object).send(:value_for, :a)).to eq 11
299
- expect(subject.represent(object).send(:value_for, :b)).to eq 22
746
+ expect(subject.represent(object).value_for(:a)).to eq 11
747
+ expect(subject.represent(object).value_for(:b)).to eq 22
300
748
  Grape::Entity.unexpose :a
301
749
  end
302
750
  end
@@ -306,17 +754,20 @@ describe Grape::Entity do
306
754
  subject.expose :name, :email
307
755
  subject.unexpose :email
308
756
 
309
- expect(subject.exposures).to eq(name: {})
757
+ expect(subject.root_exposures.length).to eq 1
758
+ expect(subject.root_exposures[0].attribute).to eq :name
310
759
  end
311
760
 
312
- context 'inherited exposures' do
761
+ context 'inherited.root_exposures' do
313
762
  it 'when called from child class, only removes from the attribute from child' do
314
763
  subject.expose :name, :email
315
764
  child_class = Class.new(subject)
316
765
  child_class.unexpose :email
317
766
 
318
- expect(child_class.exposures).to eq(name: {})
319
- expect(subject.exposures).to eq(name: {}, email: {})
767
+ expect(child_class.root_exposures.length).to eq 1
768
+ expect(child_class.root_exposures[0].attribute).to eq :name
769
+ expect(subject.root_exposures[0].attribute).to eq :name
770
+ expect(subject.root_exposures[1].attribute).to eq :email
320
771
  end
321
772
 
322
773
  context 'when called from the parent class' do
@@ -325,17 +776,30 @@ describe Grape::Entity do
325
776
  child_class = Class.new(subject)
326
777
  subject.unexpose :email
327
778
 
328
- expect(subject.exposures).to eq(name: {})
329
- expect(child_class.exposures).to eq(name: {}, email: {})
779
+ expect(subject.root_exposures.length).to eq 1
780
+ expect(subject.root_exposures[0].attribute).to eq :name
781
+ expect(child_class.root_exposures[0].attribute).to eq :name
782
+ expect(child_class.root_exposures[1].attribute).to eq :email
330
783
  end
331
784
  end
332
785
  end
333
786
 
787
+ it 'does not allow unexposing inside of nesting exposures' do
788
+ expect do
789
+ Class.new(Grape::Entity) do
790
+ expose :something do
791
+ expose :x
792
+ unexpose :x
793
+ end
794
+ end
795
+ end.to raise_error(/You cannot call 'unexpose`/)
796
+ end
797
+
334
798
  it 'works global on Grape::Entity' do
335
- Grape::Entity.expose :a
336
- expect(Grape::Entity.exposures).to eq(a: {})
337
- Grape::Entity.unexpose :a
338
- expect(Grape::Entity.exposures).to eq({})
799
+ Grape::Entity.expose :x
800
+ expect(Grape::Entity.root_exposures[0].attribute).to eq(:x)
801
+ Grape::Entity.unexpose :x
802
+ expect(Grape::Entity.root_exposures).to eq([])
339
803
  end
340
804
  end
341
805
 
@@ -350,14 +814,16 @@ describe Grape::Entity do
350
814
  expect { subject.class_eval(&block) }.to raise_error ArgumentError
351
815
  end
352
816
 
353
- it 'applies the options to all exposures inside' do
817
+ it 'applies the options to all.root_exposures inside' do
354
818
  subject.class_eval do
355
819
  with_options(if: { awesome: true }) do
356
820
  expose :awesome_thing, using: 'Awesome'
357
821
  end
358
822
  end
359
823
 
360
- expect(subject.exposures[:awesome_thing]).to eq(if: { awesome: true }, using: 'Awesome')
824
+ exposure = subject.find_exposure(:awesome_thing)
825
+ expect(exposure.using_class_name).to eq('Awesome')
826
+ expect(exposure.conditions[0].cond_hash).to eq(awesome: true)
361
827
  end
362
828
 
363
829
  it 'allows for nested .with_options' do
@@ -369,7 +835,9 @@ describe Grape::Entity do
369
835
  end
370
836
  end
371
837
 
372
- expect(subject.exposures[:awesome_thing]).to eq(if: { awesome: true }, using: 'Something')
838
+ exposure = subject.find_exposure(:awesome_thing)
839
+ expect(exposure.using_class_name).to eq('Something')
840
+ expect(exposure.conditions[0].cond_hash).to eq(awesome: true)
373
841
  end
374
842
 
375
843
  it 'overrides nested :as option' do
@@ -379,7 +847,8 @@ describe Grape::Entity do
379
847
  end
380
848
  end
381
849
 
382
- expect(subject.exposures[:awesome_thing]).to eq(as: :extra_smooth)
850
+ exposure = subject.find_exposure(:awesome_thing)
851
+ expect(exposure.key(subject)).to eq :extra_smooth
383
852
  end
384
853
 
385
854
  it 'merges nested :if option' do
@@ -401,10 +870,11 @@ describe Grape::Entity do
401
870
  end
402
871
  end
403
872
 
404
- expect(subject.exposures[:awesome_thing]).to eq(
405
- if: { awesome: false, less_awesome: true },
406
- if_extras: [:awesome, match_proc]
407
- )
873
+ exposure = subject.find_exposure(:awesome_thing)
874
+ expect(exposure.conditions.any?(&:inversed?)).to be_falsey
875
+ expect(exposure.conditions[0].symbol).to eq(:awesome)
876
+ expect(exposure.conditions[1].block).to eq(match_proc)
877
+ expect(exposure.conditions[2].cond_hash).to eq(awesome: false, less_awesome: true)
408
878
  end
409
879
 
410
880
  it 'merges nested :unless option' do
@@ -426,10 +896,11 @@ describe Grape::Entity do
426
896
  end
427
897
  end
428
898
 
429
- expect(subject.exposures[:awesome_thing]).to eq(
430
- unless: { awesome: false, less_awesome: true },
431
- unless_extras: [:awesome, match_proc]
432
- )
899
+ exposure = subject.find_exposure(:awesome_thing)
900
+ expect(exposure.conditions.all?(&:inversed?)).to be_truthy
901
+ expect(exposure.conditions[0].symbol).to eq(:awesome)
902
+ expect(exposure.conditions[1].block).to eq(match_proc)
903
+ expect(exposure.conditions[2].cond_hash).to eq(awesome: false, less_awesome: true)
433
904
  end
434
905
 
435
906
  it 'overrides nested :using option' do
@@ -439,7 +910,8 @@ describe Grape::Entity do
439
910
  end
440
911
  end
441
912
 
442
- expect(subject.exposures[:awesome_thing]).to eq(using: 'SomethingElse')
913
+ exposure = subject.find_exposure(:awesome_thing)
914
+ expect(exposure.using_class_name).to eq('SomethingElse')
443
915
  end
444
916
 
445
917
  it 'aliases :with option to :using option' do
@@ -448,7 +920,9 @@ describe Grape::Entity do
448
920
  expose :awesome_thing, with: 'SomethingElse'
449
921
  end
450
922
  end
451
- expect(subject.exposures[:awesome_thing]).to eq(using: 'SomethingElse')
923
+
924
+ exposure = subject.find_exposure(:awesome_thing)
925
+ expect(exposure.using_class_name).to eq('SomethingElse')
452
926
  end
453
927
 
454
928
  it 'overrides nested :proc option' do
@@ -460,7 +934,8 @@ describe Grape::Entity do
460
934
  end
461
935
  end
462
936
 
463
- expect(subject.exposures[:awesome_thing]).to eq(proc: match_proc)
937
+ exposure = subject.find_exposure(:awesome_thing)
938
+ expect(exposure.block).to eq(match_proc)
464
939
  end
465
940
 
466
941
  it 'overrides nested :documentation option' do
@@ -470,7 +945,36 @@ describe Grape::Entity do
470
945
  end
471
946
  end
472
947
 
473
- expect(subject.exposures[:awesome_thing]).to eq(documentation: { desc: 'Other description.' })
948
+ exposure = subject.find_exposure(:awesome_thing)
949
+ expect(exposure.documentation).to eq(desc: 'Other description.')
950
+ end
951
+
952
+ it 'propagates expose_nil option' do
953
+ subject.class_eval do
954
+ with_options(expose_nil: false) do
955
+ expose :awesome_thing
956
+ end
957
+ end
958
+
959
+ exposure = subject.find_exposure(:awesome_thing)
960
+ expect(exposure.conditions[0].inversed?).to be true
961
+ expect(exposure.conditions[0].block.call(awesome_thing: nil)).to be true
962
+ end
963
+
964
+ it 'overrides nested :expose_nil option' do
965
+ subject.class_eval do
966
+ with_options(expose_nil: true) do
967
+ expose :awesome_thing, expose_nil: false
968
+ expose :other_awesome_thing
969
+ end
970
+ end
971
+
972
+ exposure = subject.find_exposure(:awesome_thing)
973
+ expect(exposure.conditions[0].inversed?).to be true
974
+ expect(exposure.conditions[0].block.call(awesome_thing: nil)).to be true
975
+ # Conditions are only added for exposures that do not expose nil
976
+ exposure = subject.find_exposure(:other_awesome_thing)
977
+ expect(exposure.conditions[0]).to be_nil
474
978
  end
475
979
  end
476
980
 
@@ -484,14 +988,14 @@ describe Grape::Entity do
484
988
  end
485
989
 
486
990
  it 'returns multiple entities if called with a collection' do
487
- representation = subject.represent(4.times.map { Object.new })
991
+ representation = subject.represent(Array.new(4) { Object.new })
488
992
  expect(representation).to be_kind_of Array
489
993
  expect(representation.size).to eq(4)
490
994
  expect(representation.reject { |r| r.is_a?(subject) }).to be_empty
491
995
  end
492
996
 
493
997
  it 'adds the collection: true option if called with a collection' do
494
- representation = subject.represent(4.times.map { Object.new })
998
+ representation = subject.represent(Array.new(4) { Object.new })
495
999
  representation.each { |r| expect(r.options[:collection]).to be true }
496
1000
  end
497
1001
 
@@ -503,7 +1007,7 @@ describe Grape::Entity do
503
1007
 
504
1008
  it 'returns a serialized array of hashes of multiple objects if serializable: true' do
505
1009
  subject.expose(:awesome) { |_| true }
506
- representation = subject.represent(2.times.map { Object.new }, serializable: true)
1010
+ representation = subject.represent(Array.new(2) { Object.new }, serializable: true)
507
1011
  expect(representation).to eq([{ awesome: true }, { awesome: true }])
508
1012
  end
509
1013
 
@@ -529,7 +1033,7 @@ describe Grape::Entity do
529
1033
  context 'with specified fields' do
530
1034
  it 'returns only specified fields with only option' do
531
1035
  subject.expose(:id, :name, :phone)
532
- representation = subject.represent(OpenStruct.new, only: [:id, :name], serializable: true)
1036
+ representation = subject.represent(OpenStruct.new, only: %i[id name], serializable: true)
533
1037
  expect(representation).to eq(id: nil, name: nil)
534
1038
  end
535
1039
 
@@ -542,7 +1046,7 @@ describe Grape::Entity do
542
1046
  it 'returns only fields specified in the only option and not specified in the except option' do
543
1047
  subject.expose(:id, :name, :phone)
544
1048
  representation = subject.represent(OpenStruct.new,
545
- only: [:name, :phone],
1049
+ only: %i[name phone],
546
1050
  except: [:phone], serializable: true)
547
1051
  expect(representation).to eq(name: nil)
548
1052
  end
@@ -587,6 +1091,22 @@ describe Grape::Entity do
587
1091
  representation = subject.represent(object, except: [:id, 'address', { user: [:id, 'name'] }], serializable: true)
588
1092
  expect(representation).to eq(name: nil, phone: nil, user: { email: nil })
589
1093
  end
1094
+
1095
+ context 'with nested attributes' do
1096
+ before do
1097
+ subject.expose :additional do
1098
+ subject.expose :something
1099
+ end
1100
+ end
1101
+
1102
+ it 'preserves nesting' do
1103
+ expect(subject.represent({ something: 123 }, only: [{ additional: [:something] }], serializable: true)).to eq(
1104
+ additional: {
1105
+ something: 123
1106
+ }
1107
+ )
1108
+ end
1109
+ end
590
1110
  end
591
1111
 
592
1112
  it 'can specify children attributes with only' do
@@ -596,7 +1116,7 @@ describe Grape::Entity do
596
1116
  subject.expose(:id, :name, :phone)
597
1117
  subject.expose(:user, using: user_entity)
598
1118
 
599
- representation = subject.represent(OpenStruct.new(user: {}), only: [:id, :name, { user: [:name, :email] }], serializable: true)
1119
+ representation = subject.represent(OpenStruct.new(user: {}), only: [:id, :name, { user: %i[name email] }], serializable: true)
600
1120
  expect(representation).to eq(id: nil, name: nil, user: { name: nil, email: nil })
601
1121
  end
602
1122
 
@@ -619,7 +1139,7 @@ describe Grape::Entity do
619
1139
  subject.expose(:user, using: user_entity)
620
1140
 
621
1141
  representation = subject.represent(OpenStruct.new(user: {}),
622
- only: [:id, :name, :phone, user: [:id, :name, :email]],
1142
+ only: [:id, :name, :phone, { user: %i[id name email] }],
623
1143
  except: [:phone, { user: [:id] }], serializable: true)
624
1144
  expect(representation).to eq(id: nil, name: nil, user: { name: nil, email: nil })
625
1145
  end
@@ -631,7 +1151,7 @@ describe Grape::Entity do
631
1151
  subject.expose(:name)
632
1152
  end
633
1153
 
634
- representation = subject.represent(OpenStruct.new, condition: true, only: [:id, :name], serializable: true)
1154
+ representation = subject.represent(OpenStruct.new, condition: true, only: %i[id name], serializable: true)
635
1155
  expect(representation).to eq(id: nil, name: nil)
636
1156
  end
637
1157
 
@@ -641,9 +1161,81 @@ describe Grape::Entity do
641
1161
  subject.expose(:name, :mobile_phone)
642
1162
  end
643
1163
 
644
- representation = subject.represent(OpenStruct.new, condition: true, except: [:phone, :mobile_phone], serializable: true)
1164
+ representation = subject.represent(OpenStruct.new, condition: true, except: %i[phone mobile_phone], serializable: true)
645
1165
  expect(representation).to eq(id: nil, name: nil)
646
1166
  end
1167
+
1168
+ it 'choses proper exposure according to condition' do
1169
+ strategy1 = ->(_obj, _opts) { 'foo' }
1170
+ strategy2 = ->(_obj, _opts) { 'bar' }
1171
+
1172
+ subject.expose :id, proc: strategy1
1173
+ subject.expose :id, proc: strategy2
1174
+ expect(subject.represent({}, condition: false, serializable: true)).to eq(id: 'bar')
1175
+ expect(subject.represent({}, condition: true, serializable: true)).to eq(id: 'bar')
1176
+
1177
+ subject.unexpose_all
1178
+
1179
+ subject.expose :id, proc: strategy1, if: :condition
1180
+ subject.expose :id, proc: strategy2
1181
+ expect(subject.represent({}, condition: false, serializable: true)).to eq(id: 'bar')
1182
+ expect(subject.represent({}, condition: true, serializable: true)).to eq(id: 'bar')
1183
+
1184
+ subject.unexpose_all
1185
+
1186
+ subject.expose :id, proc: strategy1
1187
+ subject.expose :id, proc: strategy2, if: :condition
1188
+ expect(subject.represent({}, condition: false, serializable: true)).to eq(id: 'foo')
1189
+ expect(subject.represent({}, condition: true, serializable: true)).to eq(id: 'bar')
1190
+
1191
+ subject.unexpose_all
1192
+
1193
+ subject.expose :id, proc: strategy1, if: :condition1
1194
+ subject.expose :id, proc: strategy2, if: :condition2
1195
+ expect(subject.represent({}, condition1: false, condition2: false, serializable: true)).to eq({})
1196
+ expect(subject.represent({}, condition1: false, condition2: true, serializable: true)).to eq(id: 'bar')
1197
+ expect(subject.represent({}, condition1: true, condition2: false, serializable: true)).to eq(id: 'foo')
1198
+ expect(subject.represent({}, condition1: true, condition2: true, serializable: true)).to eq(id: 'bar')
1199
+ end
1200
+
1201
+ it 'does not merge nested exposures with plain hashes' do
1202
+ subject.expose(:id)
1203
+ subject.expose(:info, if: :condition1) do
1204
+ subject.expose :a, :b
1205
+ subject.expose(:additional, if: :condition2) do |_obj, _opts|
1206
+ {
1207
+ x: 11, y: 22, c: 123
1208
+ }
1209
+ end
1210
+ end
1211
+ subject.expose(:info, if: :condition2) do
1212
+ subject.expose(:additional) do
1213
+ subject.expose :c
1214
+ end
1215
+ end
1216
+ subject.expose(:d, as: :info, if: :condition3)
1217
+
1218
+ obj = { id: 123, a: 1, b: 2, c: 3, d: 4 }
1219
+
1220
+ expect(subject.represent(obj, serializable: true)).to eq(id: 123)
1221
+ expect(subject.represent(obj, condition1: true, serializable: true)).to eq(id: 123, info: { a: 1, b: 2 })
1222
+ expect(subject.represent(obj, condition2: true, serializable: true)).to eq(
1223
+ id: 123,
1224
+ info: {
1225
+ additional: {
1226
+ c: 3
1227
+ }
1228
+ }
1229
+ )
1230
+ expect(subject.represent(obj, condition1: true, condition2: true, serializable: true)).to eq(
1231
+ id: 123,
1232
+ info: {
1233
+ a: 1, b: 2, additional: { c: 3 }
1234
+ }
1235
+ )
1236
+ expect(subject.represent(obj, condition3: true, serializable: true)).to eq(id: 123, info: 4)
1237
+ expect(subject.represent(obj, condition1: true, condition2: true, condition3: true, serializable: true)).to eq(id: 123, info: 4)
1238
+ end
647
1239
  end
648
1240
 
649
1241
  context 'attribute with alias' do
@@ -651,7 +1243,7 @@ describe Grape::Entity do
651
1243
  subject.expose(:id)
652
1244
  subject.expose(:name, as: :title)
653
1245
 
654
- representation = subject.represent(OpenStruct.new, condition: true, only: [:id, :title], serializable: true)
1246
+ representation = subject.represent(OpenStruct.new, condition: true, only: %i[id title], serializable: true)
655
1247
  expect(representation).to eq(id: nil, title: nil)
656
1248
  end
657
1249
 
@@ -678,10 +1270,22 @@ describe Grape::Entity do
678
1270
  subject.expose(:nephew, using: nephew_entity)
679
1271
 
680
1272
  representation = subject.represent(OpenStruct.new(user: {}),
681
- only: [:id, :name, :user], except: [:nephew], serializable: true)
1273
+ only: %i[id name user], except: [:nephew], serializable: true)
682
1274
  expect(representation).to eq(id: nil, name: nil, user: { id: nil, name: nil, email: nil })
683
1275
  end
684
1276
  end
1277
+
1278
+ context 'when NameError happens in a parameterized block_exposure' do
1279
+ before do
1280
+ subject.expose :raise_no_method_error do |_|
1281
+ foo
1282
+ end
1283
+ end
1284
+
1285
+ it 'does not cause infinite loop' do
1286
+ expect { subject.represent({}, serializable: true) }.to raise_error(NameError)
1287
+ end
1288
+ end
685
1289
  end
686
1290
  end
687
1291
 
@@ -690,7 +1294,7 @@ describe Grape::Entity do
690
1294
  subject.present_collection true
691
1295
  subject.expose :items
692
1296
 
693
- representation = subject.represent(4.times.map { Object.new })
1297
+ representation = subject.represent(Array.new(4) { Object.new })
694
1298
  expect(representation).to be_kind_of(subject)
695
1299
  expect(representation.object).to be_kind_of(Hash)
696
1300
  expect(representation.object).to have_key :items
@@ -702,7 +1306,8 @@ describe Grape::Entity do
702
1306
  subject.present_collection true, :my_items
703
1307
  subject.expose :my_items
704
1308
 
705
- representation = subject.represent(4.times.map { Object.new }, serializable: true)
1309
+ representation = subject.represent(Array.new(4) { Object.new }, serializable: true)
1310
+ expect(representation).to be_kind_of(Grape::Entity::Exposure::NestingExposure::OutputBuilder)
706
1311
  expect(representation).to be_kind_of(Hash)
707
1312
  expect(representation).to have_key :my_items
708
1313
  expect(representation[:my_items]).to be_kind_of Array
@@ -727,7 +1332,7 @@ describe Grape::Entity do
727
1332
 
728
1333
  context 'with an array of objects' do
729
1334
  it 'allows a root element name to be specified' do
730
- representation = subject.represent(4.times.map { Object.new })
1335
+ representation = subject.represent(Array.new(4) { Object.new })
731
1336
  expect(representation).to be_kind_of Hash
732
1337
  expect(representation).to have_key 'things'
733
1338
  expect(representation['things']).to be_kind_of Array
@@ -738,13 +1343,13 @@ describe Grape::Entity do
738
1343
 
739
1344
  context 'it can be overridden' do
740
1345
  it 'can be disabled' do
741
- representation = subject.represent(4.times.map { Object.new }, root: false)
1346
+ representation = subject.represent(Array.new(4) { Object.new }, root: false)
742
1347
  expect(representation).to be_kind_of Array
743
1348
  expect(representation.size).to eq 4
744
1349
  expect(representation.reject { |r| r.is_a?(subject) }).to be_empty
745
1350
  end
746
1351
  it 'can use a different name' do
747
- representation = subject.represent(4.times.map { Object.new }, root: 'others')
1352
+ representation = subject.represent(Array.new(4) { Object.new }, root: 'others')
748
1353
  expect(representation).to be_kind_of Hash
749
1354
  expect(representation).to have_key 'others'
750
1355
  expect(representation['others']).to be_kind_of Array
@@ -770,7 +1375,7 @@ describe Grape::Entity do
770
1375
 
771
1376
  context 'with an array of objects' do
772
1377
  it 'allows a root element name to be specified' do
773
- representation = subject.represent(4.times.map { Object.new })
1378
+ representation = subject.represent(Array.new(4) { Object.new })
774
1379
  expect(representation).to be_kind_of Array
775
1380
  expect(representation.size).to eq 4
776
1381
  expect(representation.reject { |r| r.is_a?(subject) }).to be_empty
@@ -791,7 +1396,7 @@ describe Grape::Entity do
791
1396
 
792
1397
  context 'with an array of objects' do
793
1398
  it 'allows a root element name to be specified' do
794
- representation = subject.represent(4.times.map { Object.new })
1399
+ representation = subject.represent(Array.new(4) { Object.new })
795
1400
  expect(representation).to be_kind_of Hash
796
1401
  expect(representation).to have_key('things')
797
1402
  expect(representation['things']).to be_kind_of Array
@@ -816,7 +1421,7 @@ describe Grape::Entity do
816
1421
 
817
1422
  it 'inherits array root root' do
818
1423
  child_class = Class.new(subject)
819
- representation = child_class.represent(4.times.map { Object.new })
1424
+ representation = child_class.represent(Array.new(4) { Object.new })
820
1425
  expect(representation).to be_kind_of Hash
821
1426
  expect(representation).to have_key('things')
822
1427
  expect(representation['things']).to be_kind_of Array
@@ -856,6 +1461,11 @@ describe Grape::Entity do
856
1461
  friends: [
857
1462
  double(name: 'Friend 1', email: 'friend1@example.com', characteristics: [], fantasies: [], birthday: Time.gm(2012, 2, 27), friends: []),
858
1463
  double(name: 'Friend 2', email: 'friend2@example.com', characteristics: [], fantasies: [], birthday: Time.gm(2012, 2, 27), friends: [])
1464
+ ],
1465
+ extra: { key: 'foo', value: 'bar' },
1466
+ nested: [
1467
+ { name: 'n1', data: { key: 'ex1', value: 'v1' } },
1468
+ { name: 'n2', data: { key: 'ex2', value: 'v2' } }
859
1469
  ]
860
1470
  }
861
1471
  end
@@ -889,7 +1499,7 @@ describe Grape::Entity do
889
1499
  expect(fresh_class.new(some_class.new).serializable_hash).to eq(name: true)
890
1500
  end
891
1501
 
892
- it "does expose attributes that don't exist on the object as nil" do
1502
+ it "does expose attributes that don't exist on the object" do
893
1503
  fresh_class.expose :email, :nonexistent_attribute, :name, safe: true
894
1504
 
895
1505
  res = fresh_class.new(model).serializable_hash
@@ -898,6 +1508,13 @@ describe Grape::Entity do
898
1508
  expect(res).to have_key :name
899
1509
  end
900
1510
 
1511
+ it "does expose attributes that don't exist on the object as nil" do
1512
+ fresh_class.expose :email, :nonexistent_attribute, :name, safe: true
1513
+
1514
+ res = fresh_class.new(model).serializable_hash
1515
+ expect(res[:nonexistent_attribute]).to eq(nil)
1516
+ end
1517
+
901
1518
  it 'does expose attributes marked as safe if model is a hash object' do
902
1519
  fresh_class.expose :name, safe: true
903
1520
 
@@ -920,7 +1537,7 @@ describe Grape::Entity do
920
1537
  context 'without safe option' do
921
1538
  it 'throws an exception when an attribute is not found on the object' do
922
1539
  fresh_class.expose :name, :nonexistent_attribute
923
- expect { fresh_class.new(model).serializable_hash }.to raise_error
1540
+ expect { fresh_class.new(model).serializable_hash }.to raise_error NoMethodError
924
1541
  end
925
1542
 
926
1543
  it "exposes attributes that don't exist on the object only when they are generated by a block" do
@@ -953,6 +1570,18 @@ describe Grape::Entity do
953
1570
  expect(res).to have_key :nonexistent_attribute
954
1571
  end
955
1572
 
1573
+ it 'exposes attributes defined through module inclusion' do
1574
+ module SharedAttributes
1575
+ def a_value
1576
+ 3.14
1577
+ end
1578
+ end
1579
+ fresh_class.include(SharedAttributes)
1580
+ fresh_class.expose :a_value
1581
+ res = fresh_class.new(model).serializable_hash
1582
+ expect(res[:a_value]).to eq(3.14)
1583
+ end
1584
+
956
1585
  it 'does not expose attributes that are generated by a block but have not passed criteria' do
957
1586
  fresh_class.expose :nonexistent_attribute,
958
1587
  proc: ->(_, _) { 'I exist, but it is not yet my time to shine' },
@@ -1018,6 +1647,124 @@ describe Grape::Entity do
1018
1647
  expect(presenter.serializable_hash).to eq(name: 'abc', embedded: { a: nil, b: { abc: 'def' } })
1019
1648
  end
1020
1649
  end
1650
+
1651
+ context '#attr_path' do
1652
+ it 'for all kinds of attributes' do
1653
+ module EntitySpec
1654
+ class EmailEntity < Grape::Entity
1655
+ expose(:email, as: :addr) { |_, o| o[:attr_path].join('/') }
1656
+ end
1657
+
1658
+ class UserEntity < Grape::Entity
1659
+ expose(:name, as: :full_name) { |_, o| o[:attr_path].join('/') }
1660
+ expose :email, using: 'EntitySpec::EmailEntity'
1661
+ end
1662
+
1663
+ class ExtraEntity < Grape::Entity
1664
+ expose(:key) { |_, o| o[:attr_path].join('/') }
1665
+ expose(:value) { |_, o| o[:attr_path].join('/') }
1666
+ end
1667
+
1668
+ class NestedEntity < Grape::Entity
1669
+ expose(:name) { |_, o| o[:attr_path].join('/') }
1670
+ expose :data, using: 'EntitySpec::ExtraEntity'
1671
+ end
1672
+ end
1673
+
1674
+ fresh_class.class_eval do
1675
+ expose(:id) { |_, o| o[:attr_path].join('/') }
1676
+ expose(:foo, as: :bar) { |_, o| o[:attr_path].join('/') }
1677
+ expose :title do
1678
+ expose :full do
1679
+ expose(:prefix, as: :pref) { |_, o| o[:attr_path].join('/') }
1680
+ expose(:main) { |_, o| o[:attr_path].join('/') }
1681
+ end
1682
+ end
1683
+ expose :friends, as: :social, using: 'EntitySpec::UserEntity'
1684
+ expose :extra, using: 'EntitySpec::ExtraEntity'
1685
+ expose :nested, using: 'EntitySpec::NestedEntity'
1686
+ end
1687
+
1688
+ expect(subject.serializable_hash).to eq(
1689
+ id: 'id',
1690
+ bar: 'bar',
1691
+ title: { full: { pref: 'title/full/pref', main: 'title/full/main' } },
1692
+ social: [
1693
+ { full_name: 'social/full_name', email: { addr: 'social/email/addr' } },
1694
+ { full_name: 'social/full_name', email: { addr: 'social/email/addr' } }
1695
+ ],
1696
+ extra: { key: 'extra/key', value: 'extra/value' },
1697
+ nested: [
1698
+ { name: 'nested/name', data: { key: 'nested/data/key', value: 'nested/data/value' } },
1699
+ { name: 'nested/name', data: { key: 'nested/data/key', value: 'nested/data/value' } }
1700
+ ]
1701
+ )
1702
+ end
1703
+
1704
+ it 'allows customize path of an attribute' do
1705
+ module EntitySpec
1706
+ class CharacterEntity < Grape::Entity
1707
+ expose(:key) { |_, o| o[:attr_path].join('/') }
1708
+ expose(:value) { |_, o| o[:attr_path].join('/') }
1709
+ end
1710
+ end
1711
+
1712
+ fresh_class.class_eval do
1713
+ expose :characteristics, using: EntitySpec::CharacterEntity,
1714
+ attr_path: ->(_obj, _opts) { :character }
1715
+ end
1716
+
1717
+ expect(subject.serializable_hash).to eq(
1718
+ characteristics: [
1719
+ { key: 'character/key', value: 'character/value' }
1720
+ ]
1721
+ )
1722
+ end
1723
+
1724
+ it 'can drop one nest level by set path_for to nil' do
1725
+ module EntitySpec
1726
+ class NoPathCharacterEntity < Grape::Entity
1727
+ expose(:key) { |_, o| o[:attr_path].join('/') }
1728
+ expose(:value) { |_, o| o[:attr_path].join('/') }
1729
+ end
1730
+ end
1731
+
1732
+ fresh_class.class_eval do
1733
+ expose :characteristics, using: EntitySpec::NoPathCharacterEntity, attr_path: proc {}
1734
+ end
1735
+
1736
+ expect(subject.serializable_hash).to eq(
1737
+ characteristics: [
1738
+ { key: 'key', value: 'value' }
1739
+ ]
1740
+ )
1741
+ end
1742
+ end
1743
+
1744
+ context 'with projections passed in options' do
1745
+ it 'allows to pass different :only and :except params using the same instance' do
1746
+ fresh_class.expose :a, :b, :c
1747
+ presenter = fresh_class.new(a: 1, b: 2, c: 3)
1748
+ expect(presenter.serializable_hash(only: %i[a b])).to eq(a: 1, b: 2)
1749
+ expect(presenter.serializable_hash(only: %i[b c])).to eq(b: 2, c: 3)
1750
+ end
1751
+ end
1752
+ end
1753
+
1754
+ describe '#inspect' do
1755
+ before do
1756
+ fresh_class.class_eval do
1757
+ expose :name, :email
1758
+ end
1759
+ end
1760
+
1761
+ it 'does not serialize delegator or options' do
1762
+ data = subject.inspect
1763
+ expect(data).to include 'name='
1764
+ expect(data).to include 'email='
1765
+ expect(data).to_not include '@options'
1766
+ expect(data).to_not include '@delegator'
1767
+ end
1021
1768
  end
1022
1769
 
1023
1770
  describe '#value_for' do
@@ -1040,17 +1787,19 @@ describe Grape::Entity do
1040
1787
  end
1041
1788
 
1042
1789
  it 'passes through bare expose attributes' do
1043
- expect(subject.send(:value_for, :name)).to eq attributes[:name]
1790
+ expect(subject.value_for(:name)).to eq attributes[:name]
1044
1791
  end
1045
1792
 
1046
1793
  it 'instantiates a representation if that is called for' do
1047
- rep = subject.send(:value_for, :friends)
1794
+ rep = subject.value_for(:friends)
1048
1795
  expect(rep.reject { |r| r.is_a?(fresh_class) }).to be_empty
1049
1796
  expect(rep.first.serializable_hash[:name]).to eq 'Friend 1'
1050
1797
  expect(rep.last.serializable_hash[:name]).to eq 'Friend 2'
1051
1798
  end
1052
1799
 
1053
1800
  context 'child representations' do
1801
+ after { EntitySpec::FriendEntity.unexpose_all }
1802
+
1054
1803
  it 'disables root key name for child representations' do
1055
1804
  module EntitySpec
1056
1805
  class FriendEntity < Grape::Entity
@@ -1063,7 +1812,7 @@ describe Grape::Entity do
1063
1812
  expose :friends, using: EntitySpec::FriendEntity
1064
1813
  end
1065
1814
 
1066
- rep = subject.send(:value_for, :friends)
1815
+ rep = subject.value_for(:friends)
1067
1816
  expect(rep).to be_kind_of Array
1068
1817
  expect(rep.reject { |r| r.is_a?(EntitySpec::FriendEntity) }).to be_empty
1069
1818
  expect(rep.first.serializable_hash[:name]).to eq 'Friend 1'
@@ -1084,7 +1833,7 @@ describe Grape::Entity do
1084
1833
  end
1085
1834
  end
1086
1835
 
1087
- rep = subject.send(:value_for, :custom_friends)
1836
+ rep = subject.value_for(:custom_friends)
1088
1837
  expect(rep).to be_kind_of Array
1089
1838
  expect(rep.reject { |r| r.is_a?(EntitySpec::FriendEntity) }).to be_empty
1090
1839
  expect(rep.first.serializable_hash).to eq(name: 'Friend 1', email: 'friend1@example.com')
@@ -1105,7 +1854,7 @@ describe Grape::Entity do
1105
1854
  end
1106
1855
  end
1107
1856
 
1108
- rep = subject.send(:value_for, :first_friend)
1857
+ rep = subject.value_for(:first_friend)
1109
1858
  expect(rep).to be_kind_of EntitySpec::FriendEntity
1110
1859
  expect(rep.serializable_hash).to eq(name: 'Friend 1', email: 'friend1@example.com')
1111
1860
  end
@@ -1118,12 +1867,14 @@ describe Grape::Entity do
1118
1867
  end
1119
1868
  end
1120
1869
 
1870
+ # rubocop:disable Lint/EmptyBlock
1121
1871
  fresh_class.class_eval do
1122
1872
  expose :first_friend, using: EntitySpec::FriendEntity do |_user, _opts|
1123
1873
  end
1124
1874
  end
1875
+ # rubocop:enable Lint/EmptyBlock
1125
1876
 
1126
- rep = subject.send(:value_for, :first_friend)
1877
+ rep = subject.value_for(:first_friend)
1127
1878
  expect(rep).to be_kind_of EntitySpec::FriendEntity
1128
1879
  expect(rep.serializable_hash).to be_nil
1129
1880
  end
@@ -1140,7 +1891,7 @@ describe Grape::Entity do
1140
1891
  expose :characteristics, using: EntitySpec::CharacteristicsEntity
1141
1892
  end
1142
1893
 
1143
- rep = subject.send(:value_for, :characteristics)
1894
+ rep = subject.value_for(:characteristics)
1144
1895
  expect(rep).to be_kind_of Array
1145
1896
  expect(rep.reject { |r| r.is_a?(EntitySpec::CharacteristicsEntity) }).to be_empty
1146
1897
  expect(rep.first.serializable_hash[:key]).to eq 'hair_color'
@@ -1160,13 +1911,13 @@ describe Grape::Entity do
1160
1911
  expose :friends, using: EntitySpec::FriendEntity
1161
1912
  end
1162
1913
 
1163
- rep = subject.send(:value_for, :friends)
1914
+ rep = subject.value_for(:friends)
1164
1915
  expect(rep).to be_kind_of Array
1165
1916
  expect(rep.reject { |r| r.is_a?(EntitySpec::FriendEntity) }).to be_empty
1166
1917
  expect(rep.first.serializable_hash[:email]).to be_nil
1167
1918
  expect(rep.last.serializable_hash[:email]).to be_nil
1168
1919
 
1169
- rep = subject.send(:value_for, :friends, user_type: :admin)
1920
+ rep = subject.value_for(:friends, Grape::Entity::Options.new(user_type: :admin))
1170
1921
  expect(rep).to be_kind_of Array
1171
1922
  expect(rep.reject { |r| r.is_a?(EntitySpec::FriendEntity) }).to be_empty
1172
1923
  expect(rep.first.serializable_hash[:email]).to eq 'friend1@example.com'
@@ -1186,7 +1937,7 @@ describe Grape::Entity do
1186
1937
  expose :friends, using: EntitySpec::FriendEntity
1187
1938
  end
1188
1939
 
1189
- rep = subject.send(:value_for, :friends, collection: false)
1940
+ rep = subject.value_for(:friends, Grape::Entity::Options.new(collection: false))
1190
1941
  expect(rep).to be_kind_of Array
1191
1942
  expect(rep.reject { |r| r.is_a?(EntitySpec::FriendEntity) }).to be_empty
1192
1943
  expect(rep.first.serializable_hash[:email]).to eq 'friend1@example.com'
@@ -1195,23 +1946,24 @@ describe Grape::Entity do
1195
1946
  end
1196
1947
 
1197
1948
  it 'calls through to the proc if there is one' do
1198
- expect(subject.send(:value_for, :computed, awesome: 123)).to eq 123
1949
+ expect(subject.value_for(:computed, Grape::Entity::Options.new(awesome: 123))).to eq 123
1199
1950
  end
1200
1951
 
1201
1952
  it 'returns a formatted value if format_with is passed' do
1202
- expect(subject.send(:value_for, :birthday)).to eq '02/27/2012'
1953
+ expect(subject.value_for(:birthday)).to eq '02/27/2012'
1203
1954
  end
1204
1955
 
1205
1956
  it 'returns a formatted value if format_with is passed a lambda' do
1206
- expect(subject.send(:value_for, :fantasies)).to eq ['Nessy', 'Double Rainbows', 'Unicorns']
1957
+ expect(subject.value_for(:fantasies)).to eq ['Nessy', 'Double Rainbows', 'Unicorns']
1207
1958
  end
1208
1959
 
1209
- it 'tries instance methods on the entity first' do
1960
+ context 'delegate_attribute' do
1210
1961
  module EntitySpec
1211
1962
  class DelegatingEntity < Grape::Entity
1212
1963
  root 'friends', 'friend'
1213
1964
  expose :name
1214
1965
  expose :email
1966
+ expose :system
1215
1967
 
1216
1968
  private
1217
1969
 
@@ -1221,14 +1973,33 @@ describe Grape::Entity do
1221
1973
  end
1222
1974
  end
1223
1975
 
1224
- friend = double('Friend', name: 'joe', email: 'joe@example.com')
1225
- rep = EntitySpec::DelegatingEntity.new(friend)
1226
- expect(rep.send(:value_for, :name)).to eq 'cooler name'
1227
- expect(rep.send(:value_for, :email)).to eq 'joe@example.com'
1976
+ it 'tries instance methods on the entity first' do
1977
+ friend = double('Friend', name: 'joe', email: 'joe@example.com')
1978
+ rep = EntitySpec::DelegatingEntity.new(friend)
1979
+ expect(rep.value_for(:name)).to eq 'cooler name'
1980
+ expect(rep.value_for(:email)).to eq 'joe@example.com'
1981
+
1982
+ another_friend = double('Friend', email: 'joe@example.com')
1983
+ rep = EntitySpec::DelegatingEntity.new(another_friend)
1984
+ expect(rep.value_for(:name)).to eq 'cooler name'
1985
+ end
1986
+
1987
+ it 'does not delegate Kernel methods' do
1988
+ foo = double 'Foo', system: 'System'
1989
+ rep = EntitySpec::DelegatingEntity.new foo
1990
+ expect(rep.value_for(:system)).to eq 'System'
1991
+ end
1228
1992
 
1229
- another_friend = double('Friend', email: 'joe@example.com')
1230
- rep = EntitySpec::DelegatingEntity.new(another_friend)
1231
- expect(rep.send(:value_for, :name)).to eq 'cooler name'
1993
+ module EntitySpec
1994
+ class DerivedEntity < DelegatingEntity
1995
+ end
1996
+ end
1997
+
1998
+ it 'derived entity get methods from base entity' do
1999
+ foo = double 'Foo', name: 'joe'
2000
+ rep = EntitySpec::DerivedEntity.new foo
2001
+ expect(rep.value_for(:name)).to eq 'cooler name'
2002
+ end
1232
2003
  end
1233
2004
 
1234
2005
  context 'using' do
@@ -1244,7 +2015,7 @@ describe Grape::Entity do
1244
2015
  expose :friends, using: 'EntitySpec::UserEntity'
1245
2016
  end
1246
2017
 
1247
- rep = subject.send(:value_for, :friends)
2018
+ rep = subject.value_for(:friends)
1248
2019
  expect(rep).to be_kind_of Array
1249
2020
  expect(rep.size).to eq 2
1250
2021
  expect(rep.all? { |r| r.is_a?(EntitySpec::UserEntity) }).to be true
@@ -1255,7 +2026,7 @@ describe Grape::Entity do
1255
2026
  expose :friends, using: EntitySpec::UserEntity
1256
2027
  end
1257
2028
 
1258
- rep = subject.send(:value_for, :friends)
2029
+ rep = subject.value_for(:friends)
1259
2030
  expect(rep).to be_kind_of Array
1260
2031
  expect(rep.size).to eq 2
1261
2032
  expect(rep.all? { |r| r.is_a?(EntitySpec::UserEntity) }).to be true
@@ -1263,7 +2034,7 @@ describe Grape::Entity do
1263
2034
  end
1264
2035
  end
1265
2036
 
1266
- describe '#documentation' do
2037
+ describe '.documentation' do
1267
2038
  it 'returns an empty hash is no documentation is provided' do
1268
2039
  fresh_class.expose :name
1269
2040
 
@@ -1288,6 +2059,18 @@ describe Grape::Entity do
1288
2059
  expect(subject.documentation).to eq(label: doc, email: doc)
1289
2060
  end
1290
2061
 
2062
+ it 'resets memoization when exposing additional attributes' do
2063
+ fresh_class.expose :x, documentation: { desc: 'just x' }
2064
+ expect(fresh_class.instance_variable_get(:@documentation)).to be_nil
2065
+ doc1 = fresh_class.documentation
2066
+ expect(fresh_class.instance_variable_get(:@documentation)).not_to be_nil
2067
+ fresh_class.expose :y, documentation: { desc: 'just y' }
2068
+ expect(fresh_class.instance_variable_get(:@documentation)).to be_nil
2069
+ doc2 = fresh_class.documentation
2070
+ expect(doc1).to eq(x: { desc: 'just x' })
2071
+ expect(doc2).to eq(x: { desc: 'just x' }, y: { desc: 'just y' })
2072
+ end
2073
+
1291
2074
  context 'inherited documentation' do
1292
2075
  it 'returns documentation from ancestor' do
1293
2076
  doc = { type: 'foo', desc: 'bar' }
@@ -1326,78 +2109,14 @@ describe Grape::Entity do
1326
2109
  expect(child_class.documentation).to eq(name: doc)
1327
2110
  expect(nephew_class.documentation).to eq(name: doc, email: new_doc)
1328
2111
  end
1329
- end
1330
- end
1331
-
1332
- describe '#key_for' do
1333
- it 'returns the attribute if no :as is set' do
1334
- fresh_class.expose :name
1335
- expect(subject.class.send(:key_for, :name)).to eq :name
1336
- end
1337
2112
 
1338
- it 'returns a symbolized version of the attribute' do
1339
- fresh_class.expose :name
1340
- expect(subject.class.send(:key_for, 'name')).to eq :name
1341
- end
1342
-
1343
- it 'returns the :as alias if one exists' do
1344
- fresh_class.expose :name, as: :nombre
1345
- expect(subject.class.send(:key_for, 'name')).to eq :nombre
1346
- end
1347
- end
1348
-
1349
- describe '#conditions_met?' do
1350
- it 'only passes through hash :if exposure if all attributes match' do
1351
- exposure_options = { if: { condition1: true, condition2: true } }
1352
-
1353
- expect(subject.send(:conditions_met?, exposure_options, {})).to be false
1354
- expect(subject.send(:conditions_met?, exposure_options, condition1: true)).to be false
1355
- expect(subject.send(:conditions_met?, exposure_options, condition1: true, condition2: true)).to be true
1356
- expect(subject.send(:conditions_met?, exposure_options, condition1: false, condition2: true)).to be false
1357
- expect(subject.send(:conditions_met?, exposure_options, condition1: true, condition2: true, other: true)).to be true
1358
- end
1359
-
1360
- it 'looks for presence/truthiness if a symbol is passed' do
1361
- exposure_options = { if: :condition1 }
1362
-
1363
- expect(subject.send(:conditions_met?, exposure_options, {})).to be false
1364
- expect(subject.send(:conditions_met?, exposure_options, condition1: true)).to be true
1365
- expect(subject.send(:conditions_met?, exposure_options, condition1: false)).to be false
1366
- expect(subject.send(:conditions_met?, exposure_options, condition1: nil)).to be false
1367
- end
1368
-
1369
- it 'looks for absence/falsiness if a symbol is passed' do
1370
- exposure_options = { unless: :condition1 }
1371
-
1372
- expect(subject.send(:conditions_met?, exposure_options, {})).to be true
1373
- expect(subject.send(:conditions_met?, exposure_options, condition1: true)).to be false
1374
- expect(subject.send(:conditions_met?, exposure_options, condition1: false)).to be true
1375
- expect(subject.send(:conditions_met?, exposure_options, condition1: nil)).to be true
1376
- end
1377
-
1378
- it 'only passes through proc :if exposure if it returns truthy value' do
1379
- exposure_options = { if: ->(_, opts) { opts[:true] } }
1380
-
1381
- expect(subject.send(:conditions_met?, exposure_options, true: false)).to be false
1382
- expect(subject.send(:conditions_met?, exposure_options, true: true)).to be true
1383
- end
1384
-
1385
- it 'only passes through hash :unless exposure if any attributes do not match' do
1386
- exposure_options = { unless: { condition1: true, condition2: true } }
1387
-
1388
- expect(subject.send(:conditions_met?, exposure_options, {})).to be true
1389
- expect(subject.send(:conditions_met?, exposure_options, condition1: true)).to be false
1390
- expect(subject.send(:conditions_met?, exposure_options, condition1: true, condition2: true)).to be false
1391
- expect(subject.send(:conditions_met?, exposure_options, condition1: false, condition2: true)).to be false
1392
- expect(subject.send(:conditions_met?, exposure_options, condition1: true, condition2: true, other: true)).to be false
1393
- expect(subject.send(:conditions_met?, exposure_options, condition1: false, condition2: false)).to be true
1394
- end
1395
-
1396
- it 'only passes through proc :unless exposure if it returns falsy value' do
1397
- exposure_options = { unless: ->(_, opts) { opts[:true] == true } }
1398
-
1399
- expect(subject.send(:conditions_met?, exposure_options, true: false)).to be true
1400
- expect(subject.send(:conditions_met?, exposure_options, true: true)).to be false
2113
+ it 'includes only root exposures' do
2114
+ fresh_class.expose :name, documentation: { desc: 'foo' }
2115
+ fresh_class.expose :nesting do
2116
+ fresh_class.expose :smth, documentation: { desc: 'should not be seen' }
2117
+ end
2118
+ expect(fresh_class.documentation).to eq(name: { desc: 'foo' })
2119
+ end
1401
2120
  end
1402
2121
  end
1403
2122
 
@@ -1418,19 +2137,19 @@ describe Grape::Entity do
1418
2137
  expose :name
1419
2138
  end
1420
2139
 
1421
- expect(subject.entity_class.exposures).not_to be_empty
2140
+ expect(subject.entity_class.root_exposures).not_to be_empty
1422
2141
  end
1423
2142
 
1424
2143
  it 'is able to expose straight from the class' do
1425
2144
  subject.entity :name, :email
1426
- expect(subject.entity_class.exposures.size).to eq 2
2145
+ expect(subject.entity_class.root_exposures.size).to eq 2
1427
2146
  end
1428
2147
 
1429
- it 'is able to mix field and advanced exposures' do
2148
+ it 'is able to mix field and advanced.root_exposures' do
1430
2149
  subject.entity :name, :email do
1431
2150
  expose :third
1432
2151
  end
1433
- expect(subject.entity_class.exposures.size).to eq 3
2152
+ expect(subject.entity_class.root_exposures.size).to eq 3
1434
2153
  end
1435
2154
 
1436
2155
  context 'instance' do