grape-entity 0.6.1 → 0.7.0
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/.gitignore +5 -1
- data/.rubocop.yml +31 -0
- data/.rubocop_todo.yml +29 -16
- data/.travis.yml +15 -10
- data/CHANGELOG.md +18 -0
- data/Dangerfile +2 -0
- data/Gemfile +6 -1
- data/README.md +82 -1
- data/Rakefile +2 -2
- data/bench/serializing.rb +2 -0
- data/grape-entity.gemspec +9 -7
- data/lib/grape-entity.rb +2 -0
- data/lib/grape_entity.rb +2 -0
- data/lib/grape_entity/condition.rb +20 -11
- data/lib/grape_entity/condition/base.rb +2 -0
- 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 +2 -0
- data/lib/grape_entity/delegator/openstruct_object.rb +2 -0
- data/lib/grape_entity/delegator/plain_object.rb +2 -0
- data/lib/grape_entity/entity.rb +49 -29
- data/lib/grape_entity/exposure.rb +58 -41
- data/lib/grape_entity/exposure/base.rb +14 -3
- 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 +34 -30
- data/lib/grape_entity/exposure/nesting_exposure/nested_exposures.rb +23 -14
- data/lib/grape_entity/exposure/nesting_exposure/output_builder.rb +5 -2
- data/lib/grape_entity/exposure/represent_exposure.rb +3 -1
- data/lib/grape_entity/options.rb +41 -56
- data/lib/grape_entity/version.rb +3 -1
- data/spec/grape_entity/entity_spec.rb +202 -47
- data/spec/grape_entity/exposure/nesting_exposure/nested_exposures_spec.rb +6 -4
- data/spec/grape_entity/exposure/represent_exposure_spec.rb +2 -0
- data/spec/grape_entity/exposure_spec.rb +14 -2
- data/spec/grape_entity/hash_spec.rb +2 -0
- data/spec/grape_entity/options_spec.rb +66 -0
- data/spec/spec_helper.rb +11 -0
- metadata +28 -39
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'grape_entity/delegator/base'
|
2
4
|
require 'grape_entity/delegator/hash_object'
|
3
5
|
require 'grape_entity/delegator/openstruct_object'
|
@@ -8,15 +10,14 @@ module Grape
|
|
8
10
|
class Entity
|
9
11
|
module Delegator
|
10
12
|
def self.new(object)
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
end
|
13
|
+
delegator_klass =
|
14
|
+
if object.is_a?(Hash) then HashObject
|
15
|
+
elsif defined?(OpenStruct) && object.is_a?(OpenStruct) then OpenStructObject
|
16
|
+
elsif object.respond_to?(:fetch, true) then FetchableObject
|
17
|
+
else PlainObject
|
18
|
+
end
|
19
|
+
|
20
|
+
delegator_klass.new(object)
|
20
21
|
end
|
21
22
|
end
|
22
23
|
end
|
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
|
|
@@ -124,7 +126,26 @@ module Grape
|
|
124
126
|
# This method is the primary means by which you will declare what attributes
|
125
127
|
# should be exposed by the entity.
|
126
128
|
#
|
129
|
+
# @option options :expose_nil When set to false the associated exposure will not
|
130
|
+
# be rendered if its value is nil.
|
131
|
+
#
|
127
132
|
# @option options :as Declare an alias for the representation of this attribute.
|
133
|
+
# If a proc is presented it is evaluated in the context of the entity so object
|
134
|
+
# and the entity methods are available to it.
|
135
|
+
#
|
136
|
+
# @example as: a proc or lambda
|
137
|
+
#
|
138
|
+
# object = OpenStruct(awesomness: 'awesome_key', awesome: 'not-my-key', other: 'other-key' )
|
139
|
+
#
|
140
|
+
# class MyEntity < Grape::Entity
|
141
|
+
# expose :awesome, as: proc { object.awesomeness }
|
142
|
+
# expose :awesomeness, as: ->(object, opts) { object.other }
|
143
|
+
# end
|
144
|
+
#
|
145
|
+
# => { 'awesome_key': 'not-my-key', 'other-key': 'awesome_key' }
|
146
|
+
#
|
147
|
+
# Note the parameters passed in via the lambda syntax.
|
148
|
+
#
|
128
149
|
# @option options :if When passed a Hash, the attribute will only be exposed if the
|
129
150
|
# runtime options match all the conditions passed in. When passed a lambda, the
|
130
151
|
# lambda will execute with two arguments: the object being represented and the
|
@@ -152,6 +173,7 @@ module Grape
|
|
152
173
|
|
153
174
|
if args.size > 1
|
154
175
|
raise ArgumentError, 'You may not use the :as option on multi-attribute exposures.' if options[:as]
|
176
|
+
raise ArgumentError, 'You may not use the :expose_nil on multi-attribute exposures.' if options.key?(:expose_nil)
|
155
177
|
raise ArgumentError, 'You may not use block-setting on multi-attribute exposures.' if block_given?
|
156
178
|
end
|
157
179
|
|
@@ -167,24 +189,24 @@ module Grape
|
|
167
189
|
|
168
190
|
@documentation = nil
|
169
191
|
@nesting_stack ||= []
|
192
|
+
args.each { |attribute| build_exposure_for_attribute(attribute, @nesting_stack, options, block) }
|
193
|
+
end
|
170
194
|
|
171
|
-
|
172
|
-
|
173
|
-
exposure = Exposure.new(attribute, options)
|
195
|
+
def self.build_exposure_for_attribute(attribute, nesting_stack, options, block)
|
196
|
+
exposure_list = nesting_stack.empty? ? root_exposures : nesting_stack.last.nested_exposures
|
174
197
|
|
175
|
-
|
176
|
-
root_exposures << exposure
|
177
|
-
else
|
178
|
-
@nesting_stack.last.nested_exposures << exposure
|
179
|
-
end
|
198
|
+
exposure = Exposure.new(attribute, options)
|
180
199
|
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
200
|
+
exposure_list.delete_by(attribute) if exposure_list.select_by(attribute).all? { |exp| exp.replaceable_by?(exposure) }
|
201
|
+
|
202
|
+
exposure_list << exposure
|
203
|
+
|
204
|
+
# Nested exposures are given in a block with no parameters.
|
205
|
+
return unless exposure.nesting?
|
206
|
+
|
207
|
+
nesting_stack << exposure
|
208
|
+
block.call
|
209
|
+
nesting_stack.pop
|
188
210
|
end
|
189
211
|
|
190
212
|
# Returns exposures that have been declared for this Entity on the top level.
|
@@ -237,9 +259,7 @@ module Grape
|
|
237
259
|
# #docmentation, any exposure without a documentation key will be ignored.
|
238
260
|
def self.documentation
|
239
261
|
@documentation ||= root_exposures.each_with_object({}) do |exposure, memo|
|
240
|
-
if exposure.documentation && !exposure.documentation.empty?
|
241
|
-
memo[exposure.key] = exposure.documentation
|
242
|
-
end
|
262
|
+
memo[exposure.key] = exposure.documentation if exposure.documentation && !exposure.documentation.empty?
|
243
263
|
end
|
244
264
|
end
|
245
265
|
|
@@ -434,12 +454,8 @@ module Grape
|
|
434
454
|
|
435
455
|
def initialize(object, options = {})
|
436
456
|
@object = object
|
437
|
-
@delegator = Delegator.new
|
438
|
-
@options =
|
439
|
-
options
|
440
|
-
else
|
441
|
-
Options.new options
|
442
|
-
end
|
457
|
+
@delegator = Delegator.new(object)
|
458
|
+
@options = options.is_a?(Options) ? options : Options.new(options)
|
443
459
|
end
|
444
460
|
|
445
461
|
def root_exposures
|
@@ -474,7 +490,11 @@ module Grape
|
|
474
490
|
end
|
475
491
|
|
476
492
|
def exec_with_object(options, &block)
|
477
|
-
|
493
|
+
if block.parameters.count == 1
|
494
|
+
instance_exec(object, &block)
|
495
|
+
else
|
496
|
+
instance_exec(object, options, &block)
|
497
|
+
end
|
478
498
|
end
|
479
499
|
|
480
500
|
def exec_with_attribute(attribute, &block)
|
@@ -506,8 +526,8 @@ module Grape
|
|
506
526
|
end
|
507
527
|
|
508
528
|
# All supported options.
|
509
|
-
OPTIONS = [
|
510
|
-
|
529
|
+
OPTIONS = %i[
|
530
|
+
rewrite as if unless using with proc documentation format_with safe attr_path if_extras unless_extras merge expose_nil
|
511
531
|
].to_set.freeze
|
512
532
|
|
513
533
|
# Merges the given options with current block options.
|
@@ -517,7 +537,7 @@ module Grape
|
|
517
537
|
opts = {}
|
518
538
|
|
519
539
|
merge_logic = proc do |key, existing_val, new_val|
|
520
|
-
if [
|
540
|
+
if %i[if unless].include?(key)
|
521
541
|
if existing_val.is_a?(Hash) && new_val.is_a?(Hash)
|
522
542
|
existing_val.merge(new_val)
|
523
543
|
elsif new_val.is_a?(Hash)
|
@@ -543,7 +563,7 @@ module Grape
|
|
543
563
|
#
|
544
564
|
# @param options [Hash] Exposure options.
|
545
565
|
def self.valid_options(options)
|
546
|
-
options.
|
566
|
+
options.each_key do |key|
|
547
567
|
raise ArgumentError, "#{key.inspect} is not a valid option." unless OPTIONS.include?(key)
|
548
568
|
end
|
549
569
|
|
@@ -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,82 @@ 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) 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)
|
56
|
+
Condition.new_unless(
|
57
|
+
proc { |object, _options| Delegator.new(object).delegate(attribute).nil? }
|
58
|
+
)
|
56
59
|
end
|
57
|
-
if_conditions << options[:if] unless options[:if].nil?
|
58
60
|
|
59
|
-
|
60
|
-
|
61
|
+
def build_class_exposure(base_args, using_class, passed_proc)
|
62
|
+
exposure =
|
63
|
+
if passed_proc
|
64
|
+
build_block_exposure(base_args, passed_proc)
|
65
|
+
else
|
66
|
+
build_delegator_exposure(base_args)
|
67
|
+
end
|
68
|
+
|
69
|
+
RepresentExposure.new(*base_args, using_class, exposure)
|
70
|
+
end
|
71
|
+
|
72
|
+
def build_formatter_exposure(base_args, format_with)
|
73
|
+
if format_with.is_a? Symbol
|
74
|
+
FormatterExposure.new(*base_args, format_with)
|
75
|
+
elsif format_with.respond_to?(:call)
|
76
|
+
FormatterBlockExposure.new(*base_args, &format_with)
|
77
|
+
end
|
61
78
|
end
|
62
79
|
|
63
|
-
|
64
|
-
|
65
|
-
unless_conditions.concat(options[:unless_extras])
|
80
|
+
def build_nesting_exposure(base_args)
|
81
|
+
NestingExposure.new(*base_args)
|
66
82
|
end
|
67
|
-
unless_conditions << options[:unless] unless options[:unless].nil?
|
68
83
|
|
69
|
-
|
70
|
-
|
84
|
+
def build_block_exposure(base_args, passed_proc)
|
85
|
+
BlockExposure.new(*base_args, &passed_proc)
|
71
86
|
end
|
72
87
|
|
73
|
-
|
88
|
+
def build_delegator_exposure(base_args)
|
89
|
+
DelegatorExposure.new(*base_args)
|
90
|
+
end
|
74
91
|
end
|
75
92
|
end
|
76
93
|
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, :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,7 +13,8 @@ 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]
|
@@ -41,7 +44,7 @@ module Grape
|
|
41
44
|
end
|
42
45
|
|
43
46
|
# if we have any nesting exposures with the same name.
|
44
|
-
def deep_complex_nesting?
|
47
|
+
def deep_complex_nesting?(entity) # rubocop:disable Lint/UnusedMethodArgument
|
45
48
|
false
|
46
49
|
end
|
47
50
|
|
@@ -102,6 +105,10 @@ module Grape
|
|
102
105
|
end
|
103
106
|
end
|
104
107
|
|
108
|
+
def key(entity = nil)
|
109
|
+
@key.respond_to?(:call) ? entity.exec_with_object(@options, &@key) : @key
|
110
|
+
end
|
111
|
+
|
105
112
|
def with_attr_path(entity, options)
|
106
113
|
path_part = attr_path(entity, options)
|
107
114
|
options.with_attr_path(path_part) do
|
@@ -109,6 +116,10 @@ module Grape
|
|
109
116
|
end
|
110
117
|
end
|
111
118
|
|
119
|
+
def replaceable_by?(other)
|
120
|
+
!nesting? && !conditional? && !other.nesting? && !other.conditional?
|
121
|
+
end
|
122
|
+
|
112
123
|
protected
|
113
124
|
|
114
125
|
attr_reader :options
|