grape-entity 0.6.0 → 0.10.1

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 (54) hide show
  1. checksums.yaml +5 -5
  2. data/.coveralls.yml +1 -0
  3. data/.github/dependabot.yml +14 -0
  4. data/.github/workflows/rubocop.yml +26 -0
  5. data/.github/workflows/ruby.yml +26 -0
  6. data/.gitignore +5 -1
  7. data/.rspec +2 -1
  8. data/.rubocop.yml +82 -2
  9. data/.rubocop_todo.yml +16 -33
  10. data/CHANGELOG.md +120 -0
  11. data/Dangerfile +2 -0
  12. data/Gemfile +8 -8
  13. data/Guardfile +4 -2
  14. data/README.md +168 -7
  15. data/Rakefile +2 -2
  16. data/UPGRADING.md +19 -2
  17. data/bench/serializing.rb +7 -0
  18. data/grape-entity.gemspec +10 -8
  19. data/lib/grape-entity.rb +2 -0
  20. data/lib/grape_entity/condition/base.rb +3 -1
  21. data/lib/grape_entity/condition/block_condition.rb +3 -1
  22. data/lib/grape_entity/condition/hash_condition.rb +2 -0
  23. data/lib/grape_entity/condition/symbol_condition.rb +2 -0
  24. data/lib/grape_entity/condition.rb +20 -11
  25. data/lib/grape_entity/delegator/base.rb +7 -0
  26. data/lib/grape_entity/delegator/fetchable_object.rb +2 -0
  27. data/lib/grape_entity/delegator/hash_object.rb +4 -2
  28. data/lib/grape_entity/delegator/openstruct_object.rb +2 -0
  29. data/lib/grape_entity/delegator/plain_object.rb +2 -0
  30. data/lib/grape_entity/delegator.rb +14 -9
  31. data/lib/grape_entity/deprecated.rb +13 -0
  32. data/lib/grape_entity/entity.rb +115 -38
  33. data/lib/grape_entity/exposure/base.rb +27 -11
  34. data/lib/grape_entity/exposure/block_exposure.rb +2 -0
  35. data/lib/grape_entity/exposure/delegator_exposure.rb +2 -0
  36. data/lib/grape_entity/exposure/formatter_block_exposure.rb +2 -0
  37. data/lib/grape_entity/exposure/formatter_exposure.rb +2 -0
  38. data/lib/grape_entity/exposure/nesting_exposure/nested_exposures.rb +27 -15
  39. data/lib/grape_entity/exposure/nesting_exposure/output_builder.rb +8 -2
  40. data/lib/grape_entity/exposure/nesting_exposure.rb +36 -30
  41. data/lib/grape_entity/exposure/represent_exposure.rb +3 -1
  42. data/lib/grape_entity/exposure.rb +69 -41
  43. data/lib/grape_entity/options.rb +44 -58
  44. data/lib/grape_entity/version.rb +3 -1
  45. data/lib/grape_entity.rb +3 -0
  46. data/spec/grape_entity/entity_spec.rb +405 -48
  47. data/spec/grape_entity/exposure/nesting_exposure/nested_exposures_spec.rb +6 -4
  48. data/spec/grape_entity/exposure/represent_exposure_spec.rb +5 -3
  49. data/spec/grape_entity/exposure_spec.rb +14 -2
  50. data/spec/grape_entity/hash_spec.rb +52 -1
  51. data/spec/grape_entity/options_spec.rb +66 -0
  52. data/spec/spec_helper.rb +17 -0
  53. metadata +35 -45
  54. data/.travis.yml +0 -26
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'multi_json'
2
4
  require 'set'
3
5
 
@@ -103,7 +105,7 @@ module Grape
103
105
  @root_exposure ||= Exposure.new(nil, nesting: true)
104
106
  end
105
107
 
106
- attr_writer :root_exposure
108
+ attr_writer :root_exposure, :formatters
107
109
 
108
110
  # Returns all formatters that are registered for this and it's ancestors
109
111
  # @return [Hash] of formatters
@@ -111,7 +113,23 @@ module Grape
111
113
  @formatters ||= {}
112
114
  end
113
115
 
114
- attr_writer :formatters
116
+ def hash_access
117
+ @hash_access ||= :to_sym
118
+ end
119
+
120
+ def hash_access=(value)
121
+ @hash_access =
122
+ case value
123
+ when :to_s, :str, :string
124
+ :to_s
125
+ else
126
+ :to_sym
127
+ end
128
+ end
129
+
130
+ def delegation_opts
131
+ @delegation_opts ||= { hash_access: hash_access }
132
+ end
115
133
  end
116
134
 
117
135
  @formatters = {}
@@ -119,12 +137,33 @@ module Grape
119
137
  def self.inherited(subclass)
120
138
  subclass.root_exposure = root_exposure.dup
121
139
  subclass.formatters = formatters.dup
140
+
141
+ super
122
142
  end
123
143
 
124
144
  # This method is the primary means by which you will declare what attributes
125
145
  # should be exposed by the entity.
126
146
  #
147
+ # @option options :expose_nil When set to false the associated exposure will not
148
+ # be rendered if its value is nil.
149
+ #
127
150
  # @option options :as Declare an alias for the representation of this attribute.
151
+ # If a proc is presented it is evaluated in the context of the entity so object
152
+ # and the entity methods are available to it.
153
+ #
154
+ # @example as: a proc or lambda
155
+ #
156
+ # object = OpenStruct(awesomeness: 'awesome_key', awesome: 'not-my-key', other: 'other-key' )
157
+ #
158
+ # class MyEntity < Grape::Entity
159
+ # expose :awesome, as: proc { object.awesomeness }
160
+ # expose :awesomeness, as: ->(object, opts) { object.other }
161
+ # end
162
+ #
163
+ # => { 'awesome_key': 'not-my-key', 'other-key': 'awesome_key' }
164
+ #
165
+ # Note the parameters passed in via the lambda syntax.
166
+ #
128
167
  # @option options :if When passed a Hash, the attribute will only be exposed if the
129
168
  # runtime options match all the conditions passed in. When passed a lambda, the
130
169
  # lambda will execute with two arguments: the object being represented and the
@@ -147,17 +186,23 @@ module Grape
147
186
  # @option options :documentation Define documenation for an exposed
148
187
  # field, typically the value is a hash with two fields, type and desc.
149
188
  # @option options :merge This option allows you to merge an exposed field to the root
189
+ #
190
+ # rubocop:disable Layout/LineLength
150
191
  def self.expose(*args, &block)
151
192
  options = merge_options(args.last.is_a?(Hash) ? args.pop : {})
152
193
 
153
194
  if args.size > 1
195
+
154
196
  raise ArgumentError, 'You may not use the :as option on multi-attribute exposures.' if options[:as]
197
+ raise ArgumentError, 'You may not use the :expose_nil on multi-attribute exposures.' if options.key?(:expose_nil)
155
198
  raise ArgumentError, 'You may not use block-setting on multi-attribute exposures.' if block_given?
156
199
  end
157
200
 
158
- raise ArgumentError, 'You may not use block-setting when also using format_with' if block_given? && options[:format_with].respond_to?(:call)
159
-
160
201
  if block_given?
202
+ if options[:format_with].respond_to?(:call)
203
+ raise ArgumentError, 'You may not use block-setting when also using format_with'
204
+ end
205
+
161
206
  if block.parameters.any?
162
207
  options[:proc] = block
163
208
  else
@@ -167,24 +212,25 @@ module Grape
167
212
 
168
213
  @documentation = nil
169
214
  @nesting_stack ||= []
215
+ args.each { |attribute| build_exposure_for_attribute(attribute, @nesting_stack, options, block) }
216
+ end
217
+ # rubocop:enable Layout/LineLength
170
218
 
171
- # rubocop:disable Style/Next
172
- args.each do |attribute|
173
- exposure = Exposure.new(attribute, options)
219
+ def self.build_exposure_for_attribute(attribute, nesting_stack, options, block)
220
+ exposure_list = nesting_stack.empty? ? root_exposures : nesting_stack.last.nested_exposures
174
221
 
175
- if @nesting_stack.empty?
176
- root_exposures << exposure
177
- else
178
- @nesting_stack.last.nested_exposures << exposure
179
- end
222
+ exposure = Exposure.new(attribute, options)
180
223
 
181
- # Nested exposures are given in a block with no parameters.
182
- if exposure.nesting?
183
- @nesting_stack << exposure
184
- block.call
185
- @nesting_stack.pop
186
- end
187
- end
224
+ exposure_list.delete_by(attribute) if exposure.override?
225
+
226
+ exposure_list << exposure
227
+
228
+ # Nested exposures are given in a block with no parameters.
229
+ return unless exposure.nesting?
230
+
231
+ nesting_stack << exposure
232
+ block.call
233
+ nesting_stack.pop
188
234
  end
189
235
 
190
236
  # Returns exposures that have been declared for this Entity on the top level.
@@ -237,9 +283,7 @@ module Grape
237
283
  # #docmentation, any exposure without a documentation key will be ignored.
238
284
  def self.documentation
239
285
  @documentation ||= root_exposures.each_with_object({}) do |exposure, memo|
240
- if exposure.documentation && !exposure.documentation.empty?
241
- memo[exposure.key] = exposure.documentation
242
- end
286
+ memo[exposure.key] = exposure.documentation if exposure.documentation && !exposure.documentation.empty?
243
287
  end
244
288
  end
245
289
 
@@ -271,6 +315,7 @@ module Grape
271
315
  #
272
316
  def self.format_with(name, &block)
273
317
  raise ArgumentError, 'You must pass a block for formatters' unless block_given?
318
+
274
319
  formatters[name.to_sym] = block
275
320
  end
276
321
 
@@ -392,6 +437,7 @@ module Grape
392
437
  # @option options :only [Array] all the fields that should be returned
393
438
  # @option options :except [Array] all the fields that should not be returned
394
439
  def self.represent(objects, options = {})
440
+ @present_collection ||= nil
395
441
  if objects.respond_to?(:to_ary) && !@present_collection
396
442
  root_element = root_element(:collection_root)
397
443
  inner = objects.to_ary.map { |object| new(object, options.reverse_merge(collection: true)).presented }
@@ -409,8 +455,9 @@ module Grape
409
455
  # This method returns the entity's root or collection root node, or its parent's
410
456
  # @param root_type: either :collection_root or just :root
411
457
  def self.root_element(root_type)
412
- if instance_variable_get("@#{root_type}")
413
- instance_variable_get("@#{root_type}")
458
+ instance_variable = "@#{root_type}"
459
+ if instance_variable_defined?(instance_variable) && instance_variable_get(instance_variable)
460
+ instance_variable_get(instance_variable)
414
461
  elsif superclass.respond_to? :root_element
415
462
  superclass.root_element(root_type)
416
463
  end
@@ -432,12 +479,8 @@ module Grape
432
479
 
433
480
  def initialize(object, options = {})
434
481
  @object = object
435
- @delegator = Delegator.new object
436
- @options = if options.is_a? Options
437
- options
438
- else
439
- Options.new options
440
- end
482
+ @options = options.is_a?(Options) ? options : Options.new(options)
483
+ @delegator = Delegator.new(object)
441
484
  end
442
485
 
443
486
  def root_exposures
@@ -472,7 +515,16 @@ module Grape
472
515
  end
473
516
 
474
517
  def exec_with_object(options, &block)
475
- instance_exec(object, options, &block)
518
+ if block.parameters.count == 1
519
+ instance_exec(object, &block)
520
+ else
521
+ instance_exec(object, options, &block)
522
+ end
523
+ rescue StandardError => e
524
+ # it handles: https://github.com/ruby/ruby/blob/v3_0_0_preview1/NEWS.md#language-changes point 3, Proc
525
+ raise Grape::Entity::Deprecated.new e.message, 'in ruby 3.0' if e.is_a?(ArgumentError)
526
+
527
+ raise e
476
528
  end
477
529
 
478
530
  def exec_with_attribute(attribute, &block)
@@ -484,28 +536,53 @@ module Grape
484
536
  end
485
537
 
486
538
  def delegate_attribute(attribute)
487
- if respond_to?(attribute, true) && Grape::Entity > method(attribute).owner
539
+ if is_defined_in_entity?(attribute)
488
540
  send(attribute)
541
+ elsif delegator.accepts_options?
542
+ delegator.delegate(attribute, **self.class.delegation_opts)
489
543
  else
490
544
  delegator.delegate(attribute)
491
545
  end
492
546
  end
493
547
 
548
+ def is_defined_in_entity?(attribute)
549
+ return false unless respond_to?(attribute, true)
550
+
551
+ ancestors = self.class.ancestors
552
+ ancestors.index(Grape::Entity) > ancestors.index(method(attribute).owner)
553
+ end
554
+
494
555
  alias as_json serializable_hash
495
556
 
496
557
  def to_json(options = {})
497
- options = options.to_h if options && options.respond_to?(:to_h)
558
+ options = options.to_h if options&.respond_to?(:to_h)
498
559
  MultiJson.dump(serializable_hash(options))
499
560
  end
500
561
 
501
562
  def to_xml(options = {})
502
- options = options.to_h if options && options.respond_to?(:to_h)
563
+ options = options.to_h if options&.respond_to?(:to_h)
503
564
  serializable_hash(options).to_xml(options)
504
565
  end
505
566
 
506
567
  # All supported options.
507
- OPTIONS = [
508
- :rewrite, :as, :if, :unless, :using, :with, :proc, :documentation, :format_with, :safe, :attr_path, :if_extras, :unless_extras, :merge
568
+ OPTIONS = %i[
569
+ rewrite
570
+ as
571
+ if
572
+ unless
573
+ using
574
+ with
575
+ proc
576
+ documentation
577
+ format_with
578
+ safe
579
+ attr_path
580
+ if_extras
581
+ unless_extras
582
+ merge
583
+ expose_nil
584
+ override
585
+ default
509
586
  ].to_set.freeze
510
587
 
511
588
  # Merges the given options with current block options.
@@ -515,7 +592,7 @@ module Grape
515
592
  opts = {}
516
593
 
517
594
  merge_logic = proc do |key, existing_val, new_val|
518
- if [:if, :unless].include?(key)
595
+ if %i[if unless].include?(key)
519
596
  if existing_val.is_a?(Hash) && new_val.is_a?(Hash)
520
597
  existing_val.merge(new_val)
521
598
  elsif new_val.is_a?(Hash)
@@ -541,7 +618,7 @@ module Grape
541
618
  #
542
619
  # @param options [Hash] Exposure options.
543
620
  def self.valid_options(options)
544
- options.keys.each do |key|
621
+ options.each_key do |key|
545
622
  raise ArgumentError, "#{key.inspect} is not a valid option." unless OPTIONS.include?(key)
546
623
  end
547
624
 
@@ -1,8 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Grape
2
4
  class Entity
3
5
  module Exposure
4
6
  class Base
5
- attr_reader :attribute, :key, :is_safe, :documentation, :conditions, :for_merge
7
+ attr_reader :attribute, :is_safe, :documentation, :override, :conditions, :for_merge
6
8
 
7
9
  def self.new(attribute, options, conditions, *args, &block)
8
10
  super(attribute, options, conditions).tap { |e| e.setup(*args, &block) }
@@ -11,11 +13,14 @@ module Grape
11
13
  def initialize(attribute, options, conditions)
12
14
  @attribute = attribute.try(:to_sym)
13
15
  @options = options
14
- @key = (options[:as] || attribute).try(:to_sym)
16
+ key = options[:as] || attribute
17
+ @key = key.respond_to?(:to_sym) ? key.to_sym : key
15
18
  @is_safe = options[:safe]
19
+ @default_value = options[:default]
16
20
  @for_merge = options[:merge]
17
21
  @attr_path_proc = options[:attr_path]
18
22
  @documentation = options[:documentation]
23
+ @override = options[:override]
19
24
  @conditions = conditions
20
25
  end
21
26
 
@@ -34,15 +39,14 @@ module Grape
34
39
  @conditions == other.conditions
35
40
  end
36
41
 
37
- def setup
38
- end
42
+ def setup; end
39
43
 
40
44
  def nesting?
41
45
  false
42
46
  end
43
47
 
44
48
  # if we have any nesting exposures with the same name.
45
- def deep_complex_nesting?
49
+ def deep_complex_nesting?(entity) # rubocop:disable Lint/UnusedMethodArgument
46
50
  false
47
51
  end
48
52
 
@@ -51,7 +55,10 @@ module Grape
51
55
  if @is_safe
52
56
  is_delegatable
53
57
  else
54
- is_delegatable || raise(NoMethodError, "#{entity.class.name} missing attribute `#{@attribute}' on #{entity.object}")
58
+ is_delegatable || raise(
59
+ NoMethodError,
60
+ "#{entity.class.name} missing attribute `#{@attribute}' on #{entity.object}"
61
+ )
55
62
  end
56
63
  end
57
64
 
@@ -76,7 +83,10 @@ module Grape
76
83
  end
77
84
 
78
85
  def valid_value(entity, options)
79
- value(entity, options) if valid?(entity)
86
+ return unless valid?(entity)
87
+
88
+ output = value(entity, options)
89
+ output.blank? && @default_value.present? ? @default_value : output
80
90
  end
81
91
 
82
92
  def should_return_key?(options)
@@ -103,11 +113,17 @@ module Grape
103
113
  end
104
114
  end
105
115
 
106
- def with_attr_path(entity, options)
116
+ def key(entity = nil)
117
+ @key.respond_to?(:call) ? entity.exec_with_object(@options, &@key) : @key
118
+ end
119
+
120
+ def with_attr_path(entity, options, &block)
107
121
  path_part = attr_path(entity, options)
108
- options.with_attr_path(path_part) do
109
- yield
110
- end
122
+ options.with_attr_path(path_part, &block)
123
+ end
124
+
125
+ def override?
126
+ @override
111
127
  end
112
128
 
113
129
  protected
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Grape
2
4
  class Entity
3
5
  module Exposure
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Grape
2
4
  class Entity
3
5
  module Exposure
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Grape
2
4
  class Entity
3
5
  module Exposure
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Grape
2
4
  class Entity
3
5
  module Exposure
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Grape
2
4
  class Entity
3
5
  module Exposure
@@ -7,12 +9,17 @@ module Grape
7
9
 
8
10
  def initialize(exposures)
9
11
  @exposures = exposures
12
+ @deep_complex_nesting = nil
10
13
  end
11
14
 
12
15
  def find_by(attribute)
13
16
  @exposures.find { |e| e.attribute == attribute }
14
17
  end
15
18
 
19
+ def select_by(attribute)
20
+ @exposures.select { |e| e.attribute == attribute }
21
+ end
22
+
16
23
  def <<(exposure)
17
24
  reset_memoization!
18
25
  @exposures << exposure
@@ -29,31 +36,36 @@ module Grape
29
36
  @exposures.clear
30
37
  end
31
38
 
32
- [
33
- :each,
34
- :to_ary, :to_a,
35
- :all?,
36
- :select,
37
- :each_with_object,
38
- :[],
39
- :==,
40
- :size,
41
- :count,
42
- :length,
43
- :empty?
39
+ # rubocop:disable Style/DocumentDynamicEvalDefinition
40
+ %i[
41
+ each
42
+ to_ary to_a
43
+ all?
44
+ select
45
+ each_with_object
46
+ \[\]
47
+ ==
48
+ size
49
+ count
50
+ length
51
+ empty?
44
52
  ].each do |name|
45
- class_eval <<-RUBY, __FILE__, __LINE__
53
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
46
54
  def #{name}(*args, &block)
47
55
  @exposures.#{name}(*args, &block)
48
56
  end
49
57
  RUBY
50
58
  end
59
+ # rubocop:enable Style/DocumentDynamicEvalDefinition
51
60
 
52
61
  # Determine if we have any nesting exposures with the same name.
53
- def deep_complex_nesting?
62
+ def deep_complex_nesting?(entity)
54
63
  if @deep_complex_nesting.nil?
55
64
  all_nesting = select(&:nesting?)
56
- @deep_complex_nesting = all_nesting.group_by(&:key).any? { |_key, exposures| exposures.length > 1 }
65
+ @deep_complex_nesting =
66
+ all_nesting
67
+ .group_by { |exposure| exposure.key(entity) }
68
+ .any? { |_key, exposures| exposures.length > 1 }
57
69
  else
58
70
  @deep_complex_nesting
59
71
  end
@@ -1,11 +1,16 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Grape
2
4
  class Entity
3
5
  module Exposure
4
6
  class NestingExposure
5
7
  class OutputBuilder < SimpleDelegator
6
- def initialize
8
+ def initialize(entity)
9
+ @entity = entity
7
10
  @output_hash = {}
8
11
  @output_collection = []
12
+
13
+ super
9
14
  end
10
15
 
11
16
  def add(exposure, result)
@@ -16,9 +21,10 @@ module Grape
16
21
  # If we have an array which should not be merged - save it with a key as a hash
17
22
  # If we have hash which should be merged - save it without a key (merge)
18
23
  return unless result
24
+
19
25
  @output_hash.merge! result, &merge_strategy(exposure.for_merge)
20
26
  else
21
- @output_hash[exposure.key] = result
27
+ @output_hash[exposure.key(@entity)] = result
22
28
  end
23
29
  end
24
30
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Grape
2
4
  class Entity
3
5
  module Exposure
@@ -29,45 +31,33 @@ module Grape
29
31
  end
30
32
 
31
33
  def value(entity, options)
32
- new_options = nesting_options_for(options)
33
- output = OutputBuilder.new
34
-
35
- normalized_exposures(entity, new_options).each_with_object(output) do |exposure, out|
36
- exposure.with_attr_path(entity, new_options) do
37
- result = exposure.value(entity, new_options)
38
- out.add(exposure, result)
39
- end
34
+ map_entity_exposures(entity, options) do |exposure, nested_options|
35
+ exposure.value(entity, nested_options)
40
36
  end
41
37
  end
42
38
 
43
- def valid_value_for(key, entity, options)
44
- new_options = nesting_options_for(options)
45
-
46
- result = nil
47
- normalized_exposures(entity, new_options).select { |e| e.key == key }.each do |exposure|
48
- exposure.with_attr_path(entity, new_options) do
49
- result = exposure.valid_value(entity, new_options)
50
- end
39
+ def serializable_value(entity, options)
40
+ map_entity_exposures(entity, options) do |exposure, nested_options|
41
+ exposure.serializable_value(entity, nested_options)
51
42
  end
52
- result
53
43
  end
54
44
 
55
- def serializable_value(entity, options)
45
+ def valid_value_for(key, entity, options)
56
46
  new_options = nesting_options_for(options)
57
- output = OutputBuilder.new
58
47
 
59
- normalized_exposures(entity, new_options).each_with_object(output) do |exposure, out|
48
+ key_exposures = normalized_exposures(entity, new_options).select { |e| e.key(entity) == key }
49
+
50
+ key_exposures.map do |exposure|
60
51
  exposure.with_attr_path(entity, new_options) do
61
- result = exposure.serializable_value(entity, new_options)
62
- out.add(exposure, result)
52
+ exposure.valid_value(entity, new_options)
63
53
  end
64
- end
54
+ end.last
65
55
  end
66
56
 
67
57
  # if we have any nesting exposures with the same name.
68
- # delegate :deep_complex_nesting?, to: :nested_exposures
69
- def deep_complex_nesting?
70
- nested_exposures.deep_complex_nesting?
58
+ # delegate :deep_complex_nesting?(entity), to: :nested_exposures
59
+ def deep_complex_nesting?(entity)
60
+ nested_exposures.deep_complex_nesting?(entity)
71
61
  end
72
62
 
73
63
  private
@@ -90,16 +80,18 @@ module Grape
90
80
 
91
81
  # This method 'merges' subsequent nesting exposures with the same name if it's needed
92
82
  def normalized_exposures(entity, options)
93
- return easy_normalized_exposures(entity, options) unless deep_complex_nesting? # optimization
83
+ return easy_normalized_exposures(entity, options) unless deep_complex_nesting?(entity) # optimization
94
84
 
95
85
  table = nested_exposures.each_with_object({}) do |exposure, output|
96
86
  should_expose = exposure.with_attr_path(entity, options) do
97
87
  exposure.should_expose?(entity, options)
98
88
  end
99
89
  next unless should_expose
100
- output[exposure.key] ||= []
101
- output[exposure.key] << exposure
90
+
91
+ output[exposure.key(entity)] ||= []
92
+ output[exposure.key(entity)] << exposure
102
93
  end
94
+
103
95
  table.map do |key, exposures|
104
96
  last_exposure = exposures.last
105
97
 
@@ -111,13 +103,27 @@ module Grape
111
103
  end
112
104
  new_nested_exposures = nesting_tail.flat_map(&:nested_exposures)
113
105
  NestingExposure.new(key, {}, [], new_nested_exposures).tap do |new_exposure|
114
- new_exposure.instance_variable_set(:@deep_complex_nesting, true) if nesting_tail.any?(&:deep_complex_nesting?)
106
+ if nesting_tail.any? { |exposure| exposure.deep_complex_nesting?(entity) }
107
+ new_exposure.instance_variable_set(:@deep_complex_nesting, true)
108
+ end
115
109
  end
116
110
  else
117
111
  last_exposure
118
112
  end
119
113
  end
120
114
  end
115
+
116
+ def map_entity_exposures(entity, options)
117
+ new_options = nesting_options_for(options)
118
+ output = OutputBuilder.new(entity)
119
+
120
+ normalized_exposures(entity, new_options).each_with_object(output) do |exposure, out|
121
+ exposure.with_attr_path(entity, new_options) do
122
+ result = yield(exposure, new_options)
123
+ out.add(exposure, result)
124
+ end
125
+ end
126
+ end
121
127
  end
122
128
  end
123
129
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Grape
2
4
  class Entity
3
5
  module Exposure
@@ -21,7 +23,7 @@ module Grape
21
23
  end
22
24
 
23
25
  def value(entity, options)
24
- new_options = options.for_nesting(key)
26
+ new_options = options.for_nesting(key(entity))
25
27
  using_class.represent(@subexposure.value(entity, options), new_options)
26
28
  end
27
29