grape-entity 0.6.1 → 0.7.0

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 (47) hide show
  1. checksums.yaml +5 -5
  2. data/.coveralls.yml +1 -0
  3. data/.gitignore +5 -1
  4. data/.rubocop.yml +31 -0
  5. data/.rubocop_todo.yml +29 -16
  6. data/.travis.yml +15 -10
  7. data/CHANGELOG.md +18 -0
  8. data/Dangerfile +2 -0
  9. data/Gemfile +6 -1
  10. data/README.md +82 -1
  11. data/Rakefile +2 -2
  12. data/bench/serializing.rb +2 -0
  13. data/grape-entity.gemspec +9 -7
  14. data/lib/grape-entity.rb +2 -0
  15. data/lib/grape_entity.rb +2 -0
  16. data/lib/grape_entity/condition.rb +20 -11
  17. data/lib/grape_entity/condition/base.rb +2 -0
  18. data/lib/grape_entity/condition/block_condition.rb +3 -1
  19. data/lib/grape_entity/condition/hash_condition.rb +2 -0
  20. data/lib/grape_entity/condition/symbol_condition.rb +2 -0
  21. data/lib/grape_entity/delegator.rb +10 -9
  22. data/lib/grape_entity/delegator/base.rb +2 -0
  23. data/lib/grape_entity/delegator/fetchable_object.rb +2 -0
  24. data/lib/grape_entity/delegator/hash_object.rb +2 -0
  25. data/lib/grape_entity/delegator/openstruct_object.rb +2 -0
  26. data/lib/grape_entity/delegator/plain_object.rb +2 -0
  27. data/lib/grape_entity/entity.rb +49 -29
  28. data/lib/grape_entity/exposure.rb +58 -41
  29. data/lib/grape_entity/exposure/base.rb +14 -3
  30. data/lib/grape_entity/exposure/block_exposure.rb +2 -0
  31. data/lib/grape_entity/exposure/delegator_exposure.rb +2 -0
  32. data/lib/grape_entity/exposure/formatter_block_exposure.rb +2 -0
  33. data/lib/grape_entity/exposure/formatter_exposure.rb +2 -0
  34. data/lib/grape_entity/exposure/nesting_exposure.rb +34 -30
  35. data/lib/grape_entity/exposure/nesting_exposure/nested_exposures.rb +23 -14
  36. data/lib/grape_entity/exposure/nesting_exposure/output_builder.rb +5 -2
  37. data/lib/grape_entity/exposure/represent_exposure.rb +3 -1
  38. data/lib/grape_entity/options.rb +41 -56
  39. data/lib/grape_entity/version.rb +3 -1
  40. data/spec/grape_entity/entity_spec.rb +202 -47
  41. data/spec/grape_entity/exposure/nesting_exposure/nested_exposures_spec.rb +6 -4
  42. data/spec/grape_entity/exposure/represent_exposure_spec.rb +2 -0
  43. data/spec/grape_entity/exposure_spec.rb +14 -2
  44. data/spec/grape_entity/hash_spec.rb +2 -0
  45. data/spec/grape_entity/options_spec.rb +66 -0
  46. data/spec/spec_helper.rb +11 -0
  47. metadata +28 -39
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Grape
2
4
  class Entity
3
5
  module Condition
@@ -1,10 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Grape
2
4
  class Entity
3
5
  module Condition
4
6
  class BlockCondition < Base
5
7
  attr_reader :block
6
8
 
7
- def setup(&block)
9
+ def setup(block)
8
10
  @block = block
9
11
  end
10
12
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Grape
2
4
  class Entity
3
5
  module Condition
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Grape
2
4
  class Entity
3
5
  module Condition
@@ -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
- if object.is_a?(Hash)
12
- HashObject.new object
13
- elsif defined?(OpenStruct) && object.is_a?(OpenStruct)
14
- OpenStructObject.new object
15
- elsif object.respond_to? :fetch, true
16
- FetchableObject.new object
17
- else
18
- PlainObject.new object
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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Grape
2
4
  class Entity
3
5
  module Delegator
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Grape
2
4
  class Entity
3
5
  module Delegator
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Grape
2
4
  class Entity
3
5
  module Delegator
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Grape
2
4
  class Entity
3
5
  module Delegator
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Grape
2
4
  class Entity
3
5
  module Delegator
@@ -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
- # rubocop:disable Style/Next
172
- args.each do |attribute|
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
- if @nesting_stack.empty?
176
- root_exposures << exposure
177
- else
178
- @nesting_stack.last.nested_exposures << exposure
179
- end
198
+ exposure = Exposure.new(attribute, options)
180
199
 
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
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 object
438
- @options = if options.is_a? 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
- instance_exec(object, options, &block)
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
- :rewrite, :as, :if, :unless, :using, :with, :proc, :documentation, :format_with, :safe, :attr_path, :if_extras, :unless_extras, :merge
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 [:if, :unless].include?(key)
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.keys.each do |key|
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
- 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) 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)
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
- if_conditions.map! do |cond|
60
- Condition.new_if cond
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
- unless_conditions = []
64
- unless options[:unless_extras].nil?
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
- unless_conditions.map! do |cond|
70
- Condition.new_unless cond
84
+ def build_block_exposure(base_args, passed_proc)
85
+ BlockExposure.new(*base_args, &passed_proc)
71
86
  end
72
87
 
73
- if_conditions + unless_conditions
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, :key, :is_safe, :documentation, :conditions, :for_merge
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
- @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]
@@ -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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Grape
2
4
  class Entity
3
5
  module Exposure
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Grape
2
4
  class Entity
3
5
  module Exposure
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Grape
2
4
  class Entity
3
5
  module Exposure
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Grape
2
4
  class Entity
3
5
  module Exposure