grape-entity 0.6.0 → 0.10.1

Sign up to get free protection for your applications and to get access to all the features.
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