grape-entity 0.6.1 → 0.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/.coveralls.yml +1 -0
- data/.gitignore +5 -1
- data/.rubocop.yml +82 -2
- data/.rubocop_todo.yml +16 -33
- data/.travis.yml +18 -17
- data/CHANGELOG.md +75 -0
- data/Dangerfile +2 -0
- data/Gemfile +6 -1
- data/Guardfile +4 -2
- data/README.md +101 -4
- data/Rakefile +2 -2
- data/UPGRADING.md +31 -2
- data/bench/serializing.rb +7 -0
- data/grape-entity.gemspec +10 -10
- data/lib/grape-entity.rb +2 -0
- data/lib/grape_entity.rb +3 -0
- data/lib/grape_entity/condition.rb +20 -11
- 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/delegator.rb +10 -9
- data/lib/grape_entity/delegator/base.rb +2 -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/deprecated.rb +13 -0
- data/lib/grape_entity/entity.rb +115 -36
- data/lib/grape_entity/exposure.rb +64 -41
- data/lib/grape_entity/exposure/base.rb +21 -8
- 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.rb +36 -30
- data/lib/grape_entity/exposure/nesting_exposure/nested_exposures.rb +26 -15
- data/lib/grape_entity/exposure/nesting_exposure/output_builder.rb +10 -2
- data/lib/grape_entity/exposure/represent_exposure.rb +3 -1
- data/lib/grape_entity/options.rb +44 -58
- data/lib/grape_entity/version.rb +3 -1
- data/spec/grape_entity/entity_spec.rb +270 -47
- 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 +38 -1
- data/spec/grape_entity/options_spec.rb +66 -0
- data/spec/spec_helper.rb +17 -0
- metadata +32 -43
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,25 @@ 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
|
+
when :to_sym, :sym, :symbol
|
126
|
+
:to_sym
|
127
|
+
else
|
128
|
+
:to_sym
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def delegation_opts
|
133
|
+
@delegation_opts ||= { hash_access: hash_access }
|
134
|
+
end
|
115
135
|
end
|
116
136
|
|
117
137
|
@formatters = {}
|
@@ -119,12 +139,33 @@ module Grape
|
|
119
139
|
def self.inherited(subclass)
|
120
140
|
subclass.root_exposure = root_exposure.dup
|
121
141
|
subclass.formatters = formatters.dup
|
142
|
+
|
143
|
+
super
|
122
144
|
end
|
123
145
|
|
124
146
|
# This method is the primary means by which you will declare what attributes
|
125
147
|
# should be exposed by the entity.
|
126
148
|
#
|
149
|
+
# @option options :expose_nil When set to false the associated exposure will not
|
150
|
+
# be rendered if its value is nil.
|
151
|
+
#
|
127
152
|
# @option options :as Declare an alias for the representation of this attribute.
|
153
|
+
# If a proc is presented it is evaluated in the context of the entity so object
|
154
|
+
# and the entity methods are available to it.
|
155
|
+
#
|
156
|
+
# @example as: a proc or lambda
|
157
|
+
#
|
158
|
+
# object = OpenStruct(awesomness: 'awesome_key', awesome: 'not-my-key', other: 'other-key' )
|
159
|
+
#
|
160
|
+
# class MyEntity < Grape::Entity
|
161
|
+
# expose :awesome, as: proc { object.awesomeness }
|
162
|
+
# expose :awesomeness, as: ->(object, opts) { object.other }
|
163
|
+
# end
|
164
|
+
#
|
165
|
+
# => { 'awesome_key': 'not-my-key', 'other-key': 'awesome_key' }
|
166
|
+
#
|
167
|
+
# Note the parameters passed in via the lambda syntax.
|
168
|
+
#
|
128
169
|
# @option options :if When passed a Hash, the attribute will only be exposed if the
|
129
170
|
# runtime options match all the conditions passed in. When passed a lambda, the
|
130
171
|
# lambda will execute with two arguments: the object being represented and the
|
@@ -147,17 +188,23 @@ module Grape
|
|
147
188
|
# @option options :documentation Define documenation for an exposed
|
148
189
|
# field, typically the value is a hash with two fields, type and desc.
|
149
190
|
# @option options :merge This option allows you to merge an exposed field to the root
|
191
|
+
#
|
192
|
+
# rubocop:disable Layout/LineLength
|
150
193
|
def self.expose(*args, &block)
|
151
194
|
options = merge_options(args.last.is_a?(Hash) ? args.pop : {})
|
152
195
|
|
153
196
|
if args.size > 1
|
197
|
+
|
154
198
|
raise ArgumentError, 'You may not use the :as option on multi-attribute exposures.' if options[:as]
|
199
|
+
raise ArgumentError, 'You may not use the :expose_nil on multi-attribute exposures.' if options.key?(:expose_nil)
|
155
200
|
raise ArgumentError, 'You may not use block-setting on multi-attribute exposures.' if block_given?
|
156
201
|
end
|
157
202
|
|
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
203
|
if block_given?
|
204
|
+
if options[:format_with].respond_to?(:call)
|
205
|
+
raise ArgumentError, 'You may not use block-setting when also using format_with'
|
206
|
+
end
|
207
|
+
|
161
208
|
if block.parameters.any?
|
162
209
|
options[:proc] = block
|
163
210
|
else
|
@@ -167,24 +214,25 @@ module Grape
|
|
167
214
|
|
168
215
|
@documentation = nil
|
169
216
|
@nesting_stack ||= []
|
217
|
+
args.each { |attribute| build_exposure_for_attribute(attribute, @nesting_stack, options, block) }
|
218
|
+
end
|
219
|
+
# rubocop:enable Layout/LineLength
|
170
220
|
|
171
|
-
|
172
|
-
|
173
|
-
exposure = Exposure.new(attribute, options)
|
221
|
+
def self.build_exposure_for_attribute(attribute, nesting_stack, options, block)
|
222
|
+
exposure_list = nesting_stack.empty? ? root_exposures : nesting_stack.last.nested_exposures
|
174
223
|
|
175
|
-
|
176
|
-
root_exposures << exposure
|
177
|
-
else
|
178
|
-
@nesting_stack.last.nested_exposures << exposure
|
179
|
-
end
|
224
|
+
exposure = Exposure.new(attribute, options)
|
180
225
|
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
226
|
+
exposure_list.delete_by(attribute) if exposure.override?
|
227
|
+
|
228
|
+
exposure_list << exposure
|
229
|
+
|
230
|
+
# Nested exposures are given in a block with no parameters.
|
231
|
+
return unless exposure.nesting?
|
232
|
+
|
233
|
+
nesting_stack << exposure
|
234
|
+
block.call
|
235
|
+
nesting_stack.pop
|
188
236
|
end
|
189
237
|
|
190
238
|
# Returns exposures that have been declared for this Entity on the top level.
|
@@ -237,9 +285,7 @@ module Grape
|
|
237
285
|
# #docmentation, any exposure without a documentation key will be ignored.
|
238
286
|
def self.documentation
|
239
287
|
@documentation ||= root_exposures.each_with_object({}) do |exposure, memo|
|
240
|
-
if exposure.documentation && !exposure.documentation.empty?
|
241
|
-
memo[exposure.key] = exposure.documentation
|
242
|
-
end
|
288
|
+
memo[exposure.key] = exposure.documentation if exposure.documentation && !exposure.documentation.empty?
|
243
289
|
end
|
244
290
|
end
|
245
291
|
|
@@ -271,6 +317,7 @@ module Grape
|
|
271
317
|
#
|
272
318
|
def self.format_with(name, &block)
|
273
319
|
raise ArgumentError, 'You must pass a block for formatters' unless block_given?
|
320
|
+
|
274
321
|
formatters[name.to_sym] = block
|
275
322
|
end
|
276
323
|
|
@@ -434,12 +481,11 @@ module Grape
|
|
434
481
|
|
435
482
|
def initialize(object, options = {})
|
436
483
|
@object = object
|
437
|
-
@
|
438
|
-
@
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
end
|
484
|
+
@options = options.is_a?(Options) ? options : Options.new(options)
|
485
|
+
@delegator = Delegator.new(object)
|
486
|
+
|
487
|
+
# Why not `arity > 1`? It might be negative https://ruby-doc.org/core-2.6.6/Method.html#method-i-arity
|
488
|
+
@delegator_accepts_opts = @delegator.method(:delegate).arity != 1
|
443
489
|
end
|
444
490
|
|
445
491
|
def root_exposures
|
@@ -474,7 +520,16 @@ module Grape
|
|
474
520
|
end
|
475
521
|
|
476
522
|
def exec_with_object(options, &block)
|
477
|
-
|
523
|
+
if block.parameters.count == 1
|
524
|
+
instance_exec(object, &block)
|
525
|
+
else
|
526
|
+
instance_exec(object, options, &block)
|
527
|
+
end
|
528
|
+
rescue StandardError => e
|
529
|
+
# it handles: https://github.com/ruby/ruby/blob/v3_0_0_preview1/NEWS.md#language-changes point 3, Proc
|
530
|
+
raise Grape::Entity::Deprecated.new e.message, 'in ruby 3.0' if e.is_a?(ArgumentError)
|
531
|
+
|
532
|
+
raise e.class, e.message
|
478
533
|
end
|
479
534
|
|
480
535
|
def exec_with_attribute(attribute, &block)
|
@@ -486,28 +541,52 @@ module Grape
|
|
486
541
|
end
|
487
542
|
|
488
543
|
def delegate_attribute(attribute)
|
489
|
-
if
|
544
|
+
if is_defined_in_entity?(attribute)
|
490
545
|
send(attribute)
|
546
|
+
elsif @delegator_accepts_opts
|
547
|
+
delegator.delegate(attribute, **self.class.delegation_opts)
|
491
548
|
else
|
492
549
|
delegator.delegate(attribute)
|
493
550
|
end
|
494
551
|
end
|
495
552
|
|
553
|
+
def is_defined_in_entity?(attribute)
|
554
|
+
return false unless respond_to?(attribute, true)
|
555
|
+
|
556
|
+
ancestors = self.class.ancestors
|
557
|
+
ancestors.index(Grape::Entity) > ancestors.index(method(attribute).owner)
|
558
|
+
end
|
559
|
+
|
496
560
|
alias as_json serializable_hash
|
497
561
|
|
498
562
|
def to_json(options = {})
|
499
|
-
options = options.to_h if options
|
563
|
+
options = options.to_h if options&.respond_to?(:to_h)
|
500
564
|
MultiJson.dump(serializable_hash(options))
|
501
565
|
end
|
502
566
|
|
503
567
|
def to_xml(options = {})
|
504
|
-
options = options.to_h if options
|
568
|
+
options = options.to_h if options&.respond_to?(:to_h)
|
505
569
|
serializable_hash(options).to_xml(options)
|
506
570
|
end
|
507
571
|
|
508
572
|
# All supported options.
|
509
|
-
OPTIONS = [
|
510
|
-
|
573
|
+
OPTIONS = %i[
|
574
|
+
rewrite
|
575
|
+
as
|
576
|
+
if
|
577
|
+
unless
|
578
|
+
using
|
579
|
+
with
|
580
|
+
proc
|
581
|
+
documentation
|
582
|
+
format_with
|
583
|
+
safe
|
584
|
+
attr_path
|
585
|
+
if_extras
|
586
|
+
unless_extras
|
587
|
+
merge
|
588
|
+
expose_nil
|
589
|
+
override
|
511
590
|
].to_set.freeze
|
512
591
|
|
513
592
|
# Merges the given options with current block options.
|
@@ -517,7 +596,7 @@ module Grape
|
|
517
596
|
opts = {}
|
518
597
|
|
519
598
|
merge_logic = proc do |key, existing_val, new_val|
|
520
|
-
if [
|
599
|
+
if %i[if unless].include?(key)
|
521
600
|
if existing_val.is_a?(Hash) && new_val.is_a?(Hash)
|
522
601
|
existing_val.merge(new_val)
|
523
602
|
elsif new_val.is_a?(Hash)
|
@@ -543,7 +622,7 @@ module Grape
|
|
543
622
|
#
|
544
623
|
# @param options [Hash] Exposure options.
|
545
624
|
def self.valid_options(options)
|
546
|
-
options.
|
625
|
+
options.each_key do |key|
|
547
626
|
raise ArgumentError, "#{key.inspect} is not a valid option." unless OPTIONS.include?(key)
|
548
627
|
end
|
549
628
|
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'grape_entity/exposure/base'
|
2
4
|
require 'grape_entity/exposure/represent_exposure'
|
3
5
|
require 'grape_entity/exposure/block_exposure'
|
@@ -10,67 +12,88 @@ require 'grape_entity/condition'
|
|
10
12
|
module Grape
|
11
13
|
class Entity
|
12
14
|
module Exposure
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
if options[:proc]
|
18
|
-
block_exposure = BlockExposure.new(*base_args, &options[:proc])
|
19
|
-
else
|
20
|
-
delegator_exposure = DelegatorExposure.new(*base_args)
|
21
|
-
end
|
15
|
+
class << self
|
16
|
+
def new(attribute, options)
|
17
|
+
conditions = compile_conditions(attribute, options)
|
18
|
+
base_args = [attribute, options, conditions]
|
22
19
|
|
23
|
-
|
20
|
+
passed_proc = options[:proc]
|
24
21
|
using_class = options[:using]
|
22
|
+
format_with = options[:format_with]
|
25
23
|
|
26
|
-
if
|
27
|
-
|
24
|
+
if using_class
|
25
|
+
build_class_exposure(base_args, using_class, passed_proc)
|
26
|
+
elsif passed_proc
|
27
|
+
build_block_exposure(base_args, passed_proc)
|
28
|
+
elsif format_with
|
29
|
+
build_formatter_exposure(base_args, format_with)
|
30
|
+
elsif options[:nesting]
|
31
|
+
build_nesting_exposure(base_args)
|
28
32
|
else
|
29
|
-
|
33
|
+
build_delegator_exposure(base_args)
|
30
34
|
end
|
35
|
+
end
|
31
36
|
|
32
|
-
|
33
|
-
block_exposure
|
37
|
+
private
|
34
38
|
|
35
|
-
|
36
|
-
|
39
|
+
def compile_conditions(attribute, options)
|
40
|
+
if_conditions = [
|
41
|
+
options[:if_extras],
|
42
|
+
options[:if]
|
43
|
+
].compact.flatten.map { |cond| Condition.new_if(cond) }
|
37
44
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
end
|
45
|
+
unless_conditions = [
|
46
|
+
options[:unless_extras],
|
47
|
+
options[:unless]
|
48
|
+
].compact.flatten.map { |cond| Condition.new_unless(cond) }
|
43
49
|
|
44
|
-
|
45
|
-
NestingExposure.new(*base_args)
|
50
|
+
unless_conditions << expose_nil_condition(attribute, options) if options[:expose_nil] == false
|
46
51
|
|
47
|
-
|
48
|
-
delegator_exposure
|
52
|
+
if_conditions + unless_conditions
|
49
53
|
end
|
50
|
-
end
|
51
54
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
55
|
+
def expose_nil_condition(attribute, options)
|
56
|
+
Condition.new_unless(
|
57
|
+
proc do |object, _options|
|
58
|
+
if options[:proc].nil?
|
59
|
+
Delegator.new(object).delegate(attribute).nil?
|
60
|
+
else
|
61
|
+
exec_with_object(options, &options[:proc]).nil?
|
62
|
+
end
|
63
|
+
end
|
64
|
+
)
|
56
65
|
end
|
57
|
-
if_conditions << options[:if] unless options[:if].nil?
|
58
66
|
|
59
|
-
|
60
|
-
|
67
|
+
def build_class_exposure(base_args, using_class, passed_proc)
|
68
|
+
exposure =
|
69
|
+
if passed_proc
|
70
|
+
build_block_exposure(base_args, passed_proc)
|
71
|
+
else
|
72
|
+
build_delegator_exposure(base_args)
|
73
|
+
end
|
74
|
+
|
75
|
+
RepresentExposure.new(*base_args, using_class, exposure)
|
76
|
+
end
|
77
|
+
|
78
|
+
def build_formatter_exposure(base_args, format_with)
|
79
|
+
if format_with.is_a? Symbol
|
80
|
+
FormatterExposure.new(*base_args, format_with)
|
81
|
+
elsif format_with.respond_to?(:call)
|
82
|
+
FormatterBlockExposure.new(*base_args, &format_with)
|
83
|
+
end
|
61
84
|
end
|
62
85
|
|
63
|
-
|
64
|
-
|
65
|
-
unless_conditions.concat(options[:unless_extras])
|
86
|
+
def build_nesting_exposure(base_args)
|
87
|
+
NestingExposure.new(*base_args)
|
66
88
|
end
|
67
|
-
unless_conditions << options[:unless] unless options[:unless].nil?
|
68
89
|
|
69
|
-
|
70
|
-
|
90
|
+
def build_block_exposure(base_args, passed_proc)
|
91
|
+
BlockExposure.new(*base_args, &passed_proc)
|
71
92
|
end
|
72
93
|
|
73
|
-
|
94
|
+
def build_delegator_exposure(base_args)
|
95
|
+
DelegatorExposure.new(*base_args)
|
96
|
+
end
|
74
97
|
end
|
75
98
|
end
|
76
99
|
end
|
@@ -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,13 @@ 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]
|
16
19
|
@for_merge = options[:merge]
|
17
20
|
@attr_path_proc = options[:attr_path]
|
18
21
|
@documentation = options[:documentation]
|
22
|
+
@override = options[:override]
|
19
23
|
@conditions = conditions
|
20
24
|
end
|
21
25
|
|
@@ -41,7 +45,7 @@ module Grape
|
|
41
45
|
end
|
42
46
|
|
43
47
|
# if we have any nesting exposures with the same name.
|
44
|
-
def deep_complex_nesting?
|
48
|
+
def deep_complex_nesting?(entity) # rubocop:disable Lint/UnusedMethodArgument
|
45
49
|
false
|
46
50
|
end
|
47
51
|
|
@@ -50,7 +54,10 @@ module Grape
|
|
50
54
|
if @is_safe
|
51
55
|
is_delegatable
|
52
56
|
else
|
53
|
-
is_delegatable || raise(
|
57
|
+
is_delegatable || raise(
|
58
|
+
NoMethodError,
|
59
|
+
"#{entity.class.name} missing attribute `#{@attribute}' on #{entity.object}"
|
60
|
+
)
|
54
61
|
end
|
55
62
|
end
|
56
63
|
|
@@ -102,11 +109,17 @@ module Grape
|
|
102
109
|
end
|
103
110
|
end
|
104
111
|
|
105
|
-
def
|
112
|
+
def key(entity = nil)
|
113
|
+
@key.respond_to?(:call) ? entity.exec_with_object(@options, &@key) : @key
|
114
|
+
end
|
115
|
+
|
116
|
+
def with_attr_path(entity, options, &block)
|
106
117
|
path_part = attr_path(entity, options)
|
107
|
-
options.with_attr_path(path_part)
|
108
|
-
|
109
|
-
|
118
|
+
options.with_attr_path(path_part, &block)
|
119
|
+
end
|
120
|
+
|
121
|
+
def override?
|
122
|
+
@override
|
110
123
|
end
|
111
124
|
|
112
125
|
protected
|