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