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.
Files changed (50) hide show
  1. checksums.yaml +5 -5
  2. data/.coveralls.yml +1 -0
  3. data/.gitignore +5 -1
  4. data/.rubocop.yml +82 -2
  5. data/.rubocop_todo.yml +16 -33
  6. data/.travis.yml +18 -17
  7. data/CHANGELOG.md +75 -0
  8. data/Dangerfile +2 -0
  9. data/Gemfile +6 -1
  10. data/Guardfile +4 -2
  11. data/README.md +101 -4
  12. data/Rakefile +2 -2
  13. data/UPGRADING.md +31 -2
  14. data/bench/serializing.rb +7 -0
  15. data/grape-entity.gemspec +10 -10
  16. data/lib/grape-entity.rb +2 -0
  17. data/lib/grape_entity.rb +3 -0
  18. data/lib/grape_entity/condition.rb +20 -11
  19. data/lib/grape_entity/condition/base.rb +3 -1
  20. data/lib/grape_entity/condition/block_condition.rb +3 -1
  21. data/lib/grape_entity/condition/hash_condition.rb +2 -0
  22. data/lib/grape_entity/condition/symbol_condition.rb +2 -0
  23. data/lib/grape_entity/delegator.rb +10 -9
  24. data/lib/grape_entity/delegator/base.rb +2 -0
  25. data/lib/grape_entity/delegator/fetchable_object.rb +2 -0
  26. data/lib/grape_entity/delegator/hash_object.rb +4 -2
  27. data/lib/grape_entity/delegator/openstruct_object.rb +2 -0
  28. data/lib/grape_entity/delegator/plain_object.rb +2 -0
  29. data/lib/grape_entity/deprecated.rb +13 -0
  30. data/lib/grape_entity/entity.rb +115 -36
  31. data/lib/grape_entity/exposure.rb +64 -41
  32. data/lib/grape_entity/exposure/base.rb +21 -8
  33. data/lib/grape_entity/exposure/block_exposure.rb +2 -0
  34. data/lib/grape_entity/exposure/delegator_exposure.rb +2 -0
  35. data/lib/grape_entity/exposure/formatter_block_exposure.rb +2 -0
  36. data/lib/grape_entity/exposure/formatter_exposure.rb +2 -0
  37. data/lib/grape_entity/exposure/nesting_exposure.rb +36 -30
  38. data/lib/grape_entity/exposure/nesting_exposure/nested_exposures.rb +26 -15
  39. data/lib/grape_entity/exposure/nesting_exposure/output_builder.rb +10 -2
  40. data/lib/grape_entity/exposure/represent_exposure.rb +3 -1
  41. data/lib/grape_entity/options.rb +44 -58
  42. data/lib/grape_entity/version.rb +3 -1
  43. data/spec/grape_entity/entity_spec.rb +270 -47
  44. data/spec/grape_entity/exposure/nesting_exposure/nested_exposures_spec.rb +6 -4
  45. data/spec/grape_entity/exposure/represent_exposure_spec.rb +5 -3
  46. data/spec/grape_entity/exposure_spec.rb +14 -2
  47. data/spec/grape_entity/hash_spec.rb +38 -1
  48. data/spec/grape_entity/options_spec.rb +66 -0
  49. data/spec/spec_helper.rb +17 -0
  50. metadata +32 -43
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ class Entity
5
+ class Deprecated < StandardError
6
+ def initialize(msg, spec)
7
+ message = "DEPRECATED #{spec}: #{msg}"
8
+
9
+ super(message)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -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
- 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
+ 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
- # rubocop:disable Style/Next
172
- args.each do |attribute|
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
- if @nesting_stack.empty?
176
- root_exposures << exposure
177
- else
178
- @nesting_stack.last.nested_exposures << exposure
179
- end
224
+ exposure = Exposure.new(attribute, options)
180
225
 
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
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
- @delegator = Delegator.new object
438
- @options = if options.is_a? Options
439
- options
440
- else
441
- Options.new options
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
- instance_exec(object, options, &block)
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 respond_to?(attribute, true) && Grape::Entity > method(attribute).owner
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 && options.respond_to?(:to_h)
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 && options.respond_to?(:to_h)
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
- :rewrite, :as, :if, :unless, :using, :with, :proc, :documentation, :format_with, :safe, :attr_path, :if_extras, :unless_extras, :merge
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 [:if, :unless].include?(key)
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.keys.each do |key|
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
- def self.new(attribute, options)
14
- conditions = compile_conditions(options)
15
- base_args = [attribute, options, conditions]
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
- if options[:using]
20
+ passed_proc = options[:proc]
24
21
  using_class = options[:using]
22
+ format_with = options[:format_with]
25
23
 
26
- if options[:proc]
27
- RepresentExposure.new(*base_args, using_class, block_exposure)
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
- RepresentExposure.new(*base_args, using_class, delegator_exposure)
33
+ build_delegator_exposure(base_args)
30
34
  end
35
+ end
31
36
 
32
- elsif options[:proc]
33
- block_exposure
37
+ private
34
38
 
35
- elsif options[:format_with]
36
- format_with = options[:format_with]
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
- if format_with.is_a? Symbol
39
- FormatterExposure.new(*base_args, format_with)
40
- elsif format_with.respond_to? :call
41
- FormatterBlockExposure.new(*base_args, &format_with)
42
- end
45
+ unless_conditions = [
46
+ options[:unless_extras],
47
+ options[:unless]
48
+ ].compact.flatten.map { |cond| Condition.new_unless(cond) }
43
49
 
44
- elsif options[:nesting]
45
- NestingExposure.new(*base_args)
50
+ unless_conditions << expose_nil_condition(attribute, options) if options[:expose_nil] == false
46
51
 
47
- else
48
- delegator_exposure
52
+ if_conditions + unless_conditions
49
53
  end
50
- end
51
54
 
52
- def self.compile_conditions(options)
53
- if_conditions = []
54
- unless options[:if_extras].nil?
55
- if_conditions.concat(options[:if_extras])
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
- if_conditions.map! do |cond|
60
- Condition.new_if cond
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
- unless_conditions = []
64
- unless options[:unless_extras].nil?
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
- unless_conditions.map! do |cond|
70
- Condition.new_unless cond
90
+ def build_block_exposure(base_args, passed_proc)
91
+ BlockExposure.new(*base_args, &passed_proc)
71
92
  end
72
93
 
73
- if_conditions + unless_conditions
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, :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,13 @@ 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]
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(NoMethodError, "#{entity.class.name} missing attribute `#{@attribute}' on #{entity.object}")
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 with_attr_path(entity, options)
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) do
108
- yield
109
- end
118
+ options.with_attr_path(path_part, &block)
119
+ end
120
+
121
+ def override?
122
+ @override
110
123
  end
111
124
 
112
125
  protected