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.
- 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
|
|