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.
- checksums.yaml +5 -5
- data/.coveralls.yml +1 -0
- data/.github/dependabot.yml +14 -0
- data/.github/workflows/rubocop.yml +26 -0
- data/.github/workflows/ruby.yml +26 -0
- data/.gitignore +5 -1
- data/.rspec +2 -1
- data/.rubocop.yml +82 -2
- data/.rubocop_todo.yml +16 -33
- data/CHANGELOG.md +120 -0
- data/Dangerfile +2 -0
- data/Gemfile +8 -8
- data/Guardfile +4 -2
- data/README.md +168 -7
- data/Rakefile +2 -2
- data/UPGRADING.md +19 -2
- data/bench/serializing.rb +7 -0
- data/grape-entity.gemspec +10 -8
- data/lib/grape-entity.rb +2 -0
- data/lib/grape_entity/condition/base.rb +3 -1
- data/lib/grape_entity/condition/block_condition.rb +3 -1
- data/lib/grape_entity/condition/hash_condition.rb +2 -0
- data/lib/grape_entity/condition/symbol_condition.rb +2 -0
- data/lib/grape_entity/condition.rb +20 -11
- data/lib/grape_entity/delegator/base.rb +7 -0
- data/lib/grape_entity/delegator/fetchable_object.rb +2 -0
- data/lib/grape_entity/delegator/hash_object.rb +4 -2
- data/lib/grape_entity/delegator/openstruct_object.rb +2 -0
- data/lib/grape_entity/delegator/plain_object.rb +2 -0
- data/lib/grape_entity/delegator.rb +14 -9
- data/lib/grape_entity/deprecated.rb +13 -0
- data/lib/grape_entity/entity.rb +115 -38
- data/lib/grape_entity/exposure/base.rb +27 -11
- data/lib/grape_entity/exposure/block_exposure.rb +2 -0
- data/lib/grape_entity/exposure/delegator_exposure.rb +2 -0
- data/lib/grape_entity/exposure/formatter_block_exposure.rb +2 -0
- data/lib/grape_entity/exposure/formatter_exposure.rb +2 -0
- data/lib/grape_entity/exposure/nesting_exposure/nested_exposures.rb +27 -15
- data/lib/grape_entity/exposure/nesting_exposure/output_builder.rb +8 -2
- data/lib/grape_entity/exposure/nesting_exposure.rb +36 -30
- data/lib/grape_entity/exposure/represent_exposure.rb +3 -1
- data/lib/grape_entity/exposure.rb +69 -41
- data/lib/grape_entity/options.rb +44 -58
- data/lib/grape_entity/version.rb +3 -1
- data/lib/grape_entity.rb +3 -0
- data/spec/grape_entity/entity_spec.rb +405 -48
- data/spec/grape_entity/exposure/nesting_exposure/nested_exposures_spec.rb +6 -4
- data/spec/grape_entity/exposure/represent_exposure_spec.rb +5 -3
- data/spec/grape_entity/exposure_spec.rb +14 -2
- data/spec/grape_entity/hash_spec.rb +52 -1
- data/spec/grape_entity/options_spec.rb +66 -0
- data/spec/spec_helper.rb +17 -0
- metadata +35 -45
- data/.travis.yml +0 -26
data/lib/grape_entity/entity.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
172
|
-
|
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
|
-
|
176
|
-
root_exposures << exposure
|
177
|
-
else
|
178
|
-
@nesting_stack.last.nested_exposures << exposure
|
179
|
-
end
|
222
|
+
exposure = Exposure.new(attribute, options)
|
180
223
|
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
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
|
-
|
413
|
-
|
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
|
-
@
|
436
|
-
@
|
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
|
-
|
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
|
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
|
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
|
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
|
-
|
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 [
|
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.
|
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, :
|
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
|
-
|
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(
|
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
|
-
|
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
|
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)
|
109
|
-
|
110
|
-
|
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
|
@@ -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
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
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 =
|
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
|
-
|
33
|
-
|
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
|
44
|
-
|
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
|
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).
|
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
|
-
|
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
|
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
|
-
|
101
|
-
output[exposure.key]
|
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
|
-
|
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
|
|