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
@@ -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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Grape
2
4
  class Entity
3
5
  module Exposure
@@ -29,45 +31,33 @@ module Grape
29
31
  end
30
32
 
31
33
  def value(entity, options)
32
- new_options = nesting_options_for(options)
33
- output = OutputBuilder.new
34
-
35
- normalized_exposures(entity, new_options).each_with_object(output) do |exposure, out|
36
- exposure.with_attr_path(entity, new_options) do
37
- result = exposure.value(entity, new_options)
38
- out.add(exposure, result)
39
- end
34
+ map_entity_exposures(entity, options) do |exposure, nested_options|
35
+ exposure.value(entity, nested_options)
40
36
  end
41
37
  end
42
38
 
43
- def valid_value_for(key, entity, options)
44
- new_options = nesting_options_for(options)
45
-
46
- result = nil
47
- normalized_exposures(entity, new_options).select { |e| e.key == key }.each do |exposure|
48
- exposure.with_attr_path(entity, new_options) do
49
- result = exposure.valid_value(entity, new_options)
50
- end
39
+ def serializable_value(entity, options)
40
+ map_entity_exposures(entity, options) do |exposure, nested_options|
41
+ exposure.serializable_value(entity, nested_options)
51
42
  end
52
- result
53
43
  end
54
44
 
55
- def serializable_value(entity, options)
45
+ def valid_value_for(key, entity, options)
56
46
  new_options = nesting_options_for(options)
57
- output = OutputBuilder.new
58
47
 
59
- normalized_exposures(entity, new_options).each_with_object(output) do |exposure, out|
48
+ key_exposures = normalized_exposures(entity, new_options).select { |e| e.key(entity) == key }
49
+
50
+ key_exposures.map do |exposure|
60
51
  exposure.with_attr_path(entity, new_options) do
61
- result = exposure.serializable_value(entity, new_options)
62
- out.add(exposure, result)
52
+ exposure.valid_value(entity, new_options)
63
53
  end
64
- end
54
+ end.last
65
55
  end
66
56
 
67
57
  # if we have any nesting exposures with the same name.
68
- # delegate :deep_complex_nesting?, to: :nested_exposures
69
- def deep_complex_nesting?
70
- nested_exposures.deep_complex_nesting?
58
+ # delegate :deep_complex_nesting?(entity), to: :nested_exposures
59
+ def deep_complex_nesting?(entity)
60
+ nested_exposures.deep_complex_nesting?(entity)
71
61
  end
72
62
 
73
63
  private
@@ -90,16 +80,18 @@ module Grape
90
80
 
91
81
  # This method 'merges' subsequent nesting exposures with the same name if it's needed
92
82
  def normalized_exposures(entity, options)
93
- return easy_normalized_exposures(entity, options) unless deep_complex_nesting? # optimization
83
+ return easy_normalized_exposures(entity, options) unless deep_complex_nesting?(entity) # optimization
94
84
 
95
85
  table = nested_exposures.each_with_object({}) do |exposure, output|
96
86
  should_expose = exposure.with_attr_path(entity, options) do
97
87
  exposure.should_expose?(entity, options)
98
88
  end
99
89
  next unless should_expose
100
- output[exposure.key] ||= []
101
- output[exposure.key] << exposure
90
+
91
+ output[exposure.key(entity)] ||= []
92
+ output[exposure.key(entity)] << exposure
102
93
  end
94
+
103
95
  table.map do |key, exposures|
104
96
  last_exposure = exposures.last
105
97
 
@@ -111,13 +103,27 @@ module Grape
111
103
  end
112
104
  new_nested_exposures = nesting_tail.flat_map(&:nested_exposures)
113
105
  NestingExposure.new(key, {}, [], new_nested_exposures).tap do |new_exposure|
114
- new_exposure.instance_variable_set(:@deep_complex_nesting, true) if nesting_tail.any?(&:deep_complex_nesting?)
106
+ if nesting_tail.any? { |exposure| exposure.deep_complex_nesting?(entity) }
107
+ new_exposure.instance_variable_set(:@deep_complex_nesting, true)
108
+ end
115
109
  end
116
110
  else
117
111
  last_exposure
118
112
  end
119
113
  end
120
114
  end
115
+
116
+ def map_entity_exposures(entity, options)
117
+ new_options = nesting_options_for(options)
118
+ output = OutputBuilder.new(entity)
119
+
120
+ normalized_exposures(entity, new_options).each_with_object(output) do |exposure, out|
121
+ exposure.with_attr_path(entity, new_options) do
122
+ result = yield(exposure, new_options)
123
+ out.add(exposure, result)
124
+ end
125
+ end
126
+ end
121
127
  end
122
128
  end
123
129
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Grape
2
4
  class Entity
3
5
  module Exposure
@@ -14,6 +16,10 @@ module Grape
14
16
  @exposures.find { |e| e.attribute == attribute }
15
17
  end
16
18
 
19
+ def select_by(attribute)
20
+ @exposures.select { |e| e.attribute == attribute }
21
+ end
22
+
17
23
  def <<(exposure)
18
24
  reset_memoization!
19
25
  @exposures << exposure
@@ -30,31 +36,36 @@ module Grape
30
36
  @exposures.clear
31
37
  end
32
38
 
33
- [
34
- :each,
35
- :to_ary, :to_a,
36
- :all?,
37
- :select,
38
- :each_with_object,
39
- :[],
40
- :==,
41
- :size,
42
- :count,
43
- :length,
44
- :empty?
39
+ # rubocop:disable Style/DocumentDynamicEvalDefinition
40
+ %i[
41
+ each
42
+ to_ary to_a
43
+ all?
44
+ select
45
+ each_with_object
46
+ \[\]
47
+ ==
48
+ size
49
+ count
50
+ length
51
+ empty?
45
52
  ].each do |name|
46
- class_eval <<-RUBY, __FILE__, __LINE__
53
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
47
54
  def #{name}(*args, &block)
48
55
  @exposures.#{name}(*args, &block)
49
56
  end
50
57
  RUBY
51
58
  end
59
+ # rubocop:enable Style/DocumentDynamicEvalDefinition
52
60
 
53
61
  # Determine if we have any nesting exposures with the same name.
54
- def deep_complex_nesting?
62
+ def deep_complex_nesting?(entity)
55
63
  if @deep_complex_nesting.nil?
56
64
  all_nesting = select(&:nesting?)
57
- @deep_complex_nesting = all_nesting.group_by(&:key).any? { |_key, exposures| exposures.length > 1 }
65
+ @deep_complex_nesting =
66
+ all_nesting
67
+ .group_by { |exposure| exposure.key(entity) }
68
+ .any? { |_key, exposures| exposures.length > 1 }
58
69
  else
59
70
  @deep_complex_nesting
60
71
  end
@@ -1,11 +1,16 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Grape
2
4
  class Entity
3
5
  module Exposure
4
6
  class NestingExposure
5
7
  class OutputBuilder < SimpleDelegator
6
- def initialize
8
+ def initialize(entity)
9
+ @entity = entity
7
10
  @output_hash = {}
8
11
  @output_collection = []
12
+
13
+ super
9
14
  end
10
15
 
11
16
  def add(exposure, result)
@@ -16,9 +21,10 @@ module Grape
16
21
  # If we have an array which should not be merged - save it with a key as a hash
17
22
  # If we have hash which should be merged - save it without a key (merge)
18
23
  return unless result
24
+
19
25
  @output_hash.merge! result, &merge_strategy(exposure.for_merge)
20
26
  else
21
- @output_hash[exposure.key] = result
27
+ @output_hash[exposure.key(@entity)] = result
22
28
  end
23
29
  end
24
30
 
@@ -45,6 +51,7 @@ module Grape
45
51
  output
46
52
  end
47
53
 
54
+ # rubocop:disable Lint/EmptyBlock
48
55
  # In case if we want to solve collisions providing lambda to :merge option
49
56
  def merge_strategy(for_merge)
50
57
  if for_merge.respond_to? :call
@@ -53,6 +60,7 @@ module Grape
53
60
  -> {}
54
61
  end
55
62
  end
63
+ # rubocop:enable Lint/EmptyBlock
56
64
  end
57
65
  end
58
66
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Grape
2
4
  class Entity
3
5
  module Exposure
@@ -21,7 +23,7 @@ module Grape
21
23
  end
22
24
 
23
25
  def value(entity, options)
24
- new_options = options.for_nesting(key)
26
+ new_options = options.for_nesting(key(entity))
25
27
  using_class.represent(@subexposure.value(entity, options), new_options)
26
28
  end
27
29
 
@@ -1,8 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+
1
5
  module Grape
2
6
  class Entity
3
7
  class Options
8
+ extend Forwardable
9
+
4
10
  attr_reader :opts_hash
5
11
 
12
+ def_delegators :opts_hash, :dig, :key?, :fetch, :[], :empty
13
+
6
14
  def initialize(opts_hash = {})
7
15
  @opts_hash = opts_hash
8
16
  @has_only = !opts_hash[:only].nil?
@@ -11,54 +19,33 @@ module Grape
11
19
  @should_return_key_cache = {}
12
20
  end
13
21
 
14
- def [](key)
15
- @opts_hash[key]
16
- end
22
+ def merge(new_opts)
23
+ return self if new_opts.empty?
17
24
 
18
- def fetch(*args)
19
- @opts_hash.fetch(*args)
20
- end
25
+ merged = if new_opts.instance_of? Options
26
+ @opts_hash.merge(new_opts.opts_hash)
27
+ else
28
+ @opts_hash.merge(new_opts)
29
+ end
21
30
 
22
- def key?(key)
23
- @opts_hash.key? key
24
- end
25
-
26
- def merge(new_opts)
27
- if new_opts.empty?
28
- self
29
- else
30
- merged = if new_opts.instance_of? Options
31
- @opts_hash.merge(new_opts.opts_hash)
32
- else
33
- @opts_hash.merge(new_opts)
34
- end
35
- Options.new(merged)
36
- end
31
+ Options.new(merged)
37
32
  end
38
33
 
39
34
  def reverse_merge(new_opts)
40
- if new_opts.empty?
41
- self
42
- else
43
- merged = if new_opts.instance_of? Options
44
- new_opts.opts_hash.merge(@opts_hash)
45
- else
46
- new_opts.merge(@opts_hash)
47
- end
48
- Options.new(merged)
49
- end
50
- end
35
+ return self if new_opts.empty?
51
36
 
52
- def empty?
53
- @opts_hash.empty?
37
+ merged = if new_opts.instance_of? Options
38
+ new_opts.opts_hash.merge(@opts_hash)
39
+ else
40
+ new_opts.merge(@opts_hash)
41
+ end
42
+
43
+ Options.new(merged)
54
44
  end
55
45
 
56
46
  def ==(other)
57
- @opts_hash == if other.is_a? Options
58
- other.opts_hash
59
- else
60
- other
61
- end
47
+ other_hash = other.is_a?(Options) ? other.opts_hash : other
48
+ @opts_hash == other_hash
62
49
  end
63
50
 
64
51
  def should_return_key?(key)
@@ -66,7 +53,7 @@ module Grape
66
53
 
67
54
  only = only_fields.nil? ||
68
55
  only_fields.key?(key)
69
- except = except_fields && except_fields.key?(key) &&
56
+ except = except_fields&.key?(key) &&
70
57
  except_fields[key] == true
71
58
  only && !except
72
59
  end
@@ -96,36 +83,35 @@ module Grape
96
83
  end
97
84
 
98
85
  def with_attr_path(part)
86
+ return yield unless part
87
+
99
88
  stack = (opts_hash[:attr_path] ||= [])
100
- if part
101
- stack.push part
102
- result = yield
103
- stack.pop
104
- result
105
- else
106
- yield
107
- end
89
+ stack.push part
90
+ result = yield
91
+ stack.pop
92
+ result
108
93
  end
109
94
 
110
95
  private
111
96
 
112
97
  def build_for_nesting(key)
113
- new_opts_hash = opts_hash.dup
114
- new_opts_hash.delete(:collection)
115
- new_opts_hash[:root] = nil
116
- new_opts_hash[:only] = only_fields(key)
117
- new_opts_hash[:except] = except_fields(key)
118
- new_opts_hash[:attr_path] = opts_hash[:attr_path]
119
-
120
- Options.new(new_opts_hash)
98
+ Options.new(
99
+ opts_hash.dup.reject { |current_key| current_key == :collection }.merge(
100
+ root: nil,
101
+ only: only_fields(key),
102
+ except: except_fields(key),
103
+ attr_path: opts_hash[:attr_path]
104
+ )
105
+ )
121
106
  end
122
107
 
123
108
  def build_symbolized_hash(attribute, hash)
124
- if attribute.is_a?(Hash)
109
+ case attribute
110
+ when Hash
125
111
  attribute.each do |attr, nested_attrs|
126
112
  hash[attr.to_sym] = build_symbolized_hash(nested_attrs, {})
127
113
  end
128
- elsif attribute.is_a?(Array)
114
+ when Array
129
115
  return attribute.each { |x| build_symbolized_hash(x, {}) }
130
116
  else
131
117
  hash[attribute.to_sym] = true
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module GrapeEntity
2
- VERSION = '0.6.1'.freeze
4
+ VERSION = '0.8.2'
3
5
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'spec_helper'
2
4
  require 'ostruct'
3
5
 
@@ -27,7 +29,11 @@ describe Grape::Entity do
27
29
  end
28
30
 
29
31
  it 'makes sure that :format_with as a proc cannot be used with a block' do
30
- expect { subject.expose :name, format_with: proc {} {} }.to raise_error ArgumentError
32
+ # rubocop:disable Style/BlockDelimiters
33
+ # rubocop:disable Lint/EmptyBlock
34
+ expect { subject.expose :name, format_with: proc {} do p 'hi' end }.to raise_error ArgumentError
35
+ # rubocop:enable Lint/EmptyBlock
36
+ # rubocop:enable Style/BlockDelimiters
31
37
  end
32
38
 
33
39
  it 'makes sure unknown options are not silently ignored' do
@@ -64,6 +70,150 @@ describe Grape::Entity do
64
70
  end
65
71
  end
66
72
 
73
+ context 'with :expose_nil option' do
74
+ let(:a) { nil }
75
+ let(:b) { nil }
76
+ let(:c) { 'value' }
77
+
78
+ context 'when model is a PORO' do
79
+ let(:model) { Model.new(a, b, c) }
80
+
81
+ before do
82
+ stub_const 'Model', Class.new
83
+ Model.class_eval do
84
+ attr_accessor :a, :b, :c
85
+
86
+ def initialize(a, b, c)
87
+ @a = a
88
+ @b = b
89
+ @c = c
90
+ end
91
+ end
92
+ end
93
+
94
+ context 'when expose_nil option is not provided' do
95
+ it 'exposes nil attributes' do
96
+ subject.expose(:a)
97
+ subject.expose(:b)
98
+ subject.expose(:c)
99
+ expect(subject.represent(model).serializable_hash).to eq(a: nil, b: nil, c: 'value')
100
+ end
101
+ end
102
+
103
+ context 'when expose_nil option is true' do
104
+ it 'exposes nil attributes' do
105
+ subject.expose(:a, expose_nil: true)
106
+ subject.expose(:b, expose_nil: true)
107
+ subject.expose(:c)
108
+ expect(subject.represent(model).serializable_hash).to eq(a: nil, b: nil, c: 'value')
109
+ end
110
+ end
111
+
112
+ context 'when expose_nil option is false' do
113
+ it 'does not expose nil attributes' do
114
+ subject.expose(:a, expose_nil: false)
115
+ subject.expose(:b, expose_nil: false)
116
+ subject.expose(:c)
117
+ expect(subject.represent(model).serializable_hash).to eq(c: 'value')
118
+ end
119
+
120
+ it 'is only applied per attribute' do
121
+ subject.expose(:a, expose_nil: false)
122
+ subject.expose(:b)
123
+ subject.expose(:c)
124
+ expect(subject.represent(model).serializable_hash).to eq(b: nil, c: 'value')
125
+ end
126
+
127
+ it 'raises an error when applied to multiple attribute exposures' do
128
+ expect { subject.expose(:a, :b, :c, expose_nil: false) }.to raise_error ArgumentError
129
+ end
130
+ end
131
+
132
+ context 'when expose_nil option is false and block passed' do
133
+ it 'does not expose if block returns nil' do
134
+ subject.expose(:a, expose_nil: false) do |_obj, _options|
135
+ nil
136
+ end
137
+ subject.expose(:b)
138
+ subject.expose(:c)
139
+ expect(subject.represent(model).serializable_hash).to eq(b: nil, c: 'value')
140
+ end
141
+
142
+ it 'exposes is block returns a value' do
143
+ subject.expose(:a, expose_nil: false) do |_obj, _options|
144
+ 100
145
+ end
146
+ subject.expose(:b)
147
+ subject.expose(:c)
148
+ expect(subject.represent(model).serializable_hash).to eq(a: 100, b: nil, c: 'value')
149
+ end
150
+ end
151
+ end
152
+
153
+ context 'when model is a hash' do
154
+ let(:model) { { a: a, b: b, c: c } }
155
+
156
+ context 'when expose_nil option is not provided' do
157
+ it 'exposes nil attributes' do
158
+ subject.expose(:a)
159
+ subject.expose(:b)
160
+ subject.expose(:c)
161
+ expect(subject.represent(model).serializable_hash).to eq(a: nil, b: nil, c: 'value')
162
+ end
163
+ end
164
+
165
+ context 'when expose_nil option is true' do
166
+ it 'exposes nil attributes' do
167
+ subject.expose(:a, expose_nil: true)
168
+ subject.expose(:b, expose_nil: true)
169
+ subject.expose(:c)
170
+ expect(subject.represent(model).serializable_hash).to eq(a: nil, b: nil, c: 'value')
171
+ end
172
+ end
173
+
174
+ context 'when expose_nil option is false' do
175
+ it 'does not expose nil attributes' do
176
+ subject.expose(:a, expose_nil: false)
177
+ subject.expose(:b, expose_nil: false)
178
+ subject.expose(:c)
179
+ expect(subject.represent(model).serializable_hash).to eq(c: 'value')
180
+ end
181
+
182
+ it 'is only applied per attribute' do
183
+ subject.expose(:a, expose_nil: false)
184
+ subject.expose(:b)
185
+ subject.expose(:c)
186
+ expect(subject.represent(model).serializable_hash).to eq(b: nil, c: 'value')
187
+ end
188
+
189
+ it 'raises an error when applied to multiple attribute exposures' do
190
+ expect { subject.expose(:a, :b, :c, expose_nil: false) }.to raise_error ArgumentError
191
+ end
192
+ end
193
+ end
194
+
195
+ context 'with nested structures' do
196
+ let(:model) { { a: a, b: b, c: { d: nil, e: nil, f: { g: nil, h: nil } } } }
197
+
198
+ context 'when expose_nil option is false' do
199
+ it 'does not expose nil attributes' do
200
+ subject.expose(:a, expose_nil: false)
201
+ subject.expose(:b)
202
+ subject.expose(:c) do
203
+ subject.expose(:d, expose_nil: false)
204
+ subject.expose(:e)
205
+ subject.expose(:f) do
206
+ subject.expose(:g, expose_nil: false)
207
+ subject.expose(:h)
208
+ end
209
+ end
210
+
211
+ expect(subject.represent(model).serializable_hash).to eq(b: nil, c: { e: nil, f: { h: nil } })
212
+ end
213
+ end
214
+ end
215
+ end
216
+
67
217
  context 'with a block' do
68
218
  it 'errors out if called with multiple attributes' do
69
219
  expect { subject.expose(:name, :email) { true } }.to raise_error ArgumentError
@@ -112,6 +262,53 @@ describe Grape::Entity do
112
262
  end
113
263
  end
114
264
 
265
+ describe 'blocks' do
266
+ class SomeObject
267
+ def method_without_args
268
+ 'result'
269
+ end
270
+ end
271
+
272
+ describe 'with block passed in' do
273
+ specify do
274
+ subject.expose :that_method_without_args do |object|
275
+ object.method_without_args
276
+ end
277
+
278
+ object = SomeObject.new
279
+
280
+ value = subject.represent(object).value_for(:that_method_without_args)
281
+ expect(value).to eq('result')
282
+ end
283
+ end
284
+
285
+ context 'with block passed in via &' do
286
+ if RUBY_VERSION.start_with?('3')
287
+ specify do
288
+ subject.expose :that_method_without_args, &:method_without_args
289
+ subject.expose :method_without_args, as: :that_method_without_args_again
290
+
291
+ object = SomeObject.new
292
+ expect do
293
+ subject.represent(object).value_for(:that_method_without_args)
294
+ end.to raise_error Grape::Entity::Deprecated
295
+
296
+ value2 = subject.represent(object).value_for(:that_method_without_args_again)
297
+ expect(value2).to eq('result')
298
+ end
299
+ else
300
+ specify do
301
+ subject.expose :that_method_without_args_again, &:method_without_args
302
+
303
+ object = SomeObject.new
304
+
305
+ value2 = subject.represent(object).value_for(:that_method_without_args_again)
306
+ expect(value2).to eq('result')
307
+ end
308
+ end
309
+ end
310
+ end
311
+
115
312
  context 'with no parameters passed to the block' do
116
313
  it 'adds a nested exposure' do
117
314
  subject.expose :awesome do
@@ -131,7 +328,7 @@ describe Grape::Entity do
131
328
  expect(another_nested).to_not be_nil
132
329
  expect(another_nested.using_class_name).to eq('Awesome')
133
330
  expect(moar_nested).to_not be_nil
134
- expect(moar_nested.key).to eq(:weee)
331
+ expect(moar_nested.key(subject)).to eq(:weee)
135
332
  end
136
333
 
137
334
  it 'represents the exposure as a hash of its nested.root_exposures' do
@@ -324,6 +521,24 @@ describe Grape::Entity do
324
521
  expect(subject.represent({ name: 'bar' }, serializable: true)).to eq(email: nil, name: 'bar')
325
522
  expect(child_class.represent({ name: 'bar' }, serializable: true)).to eq(email: nil, name: 'foo')
326
523
  end
524
+
525
+ it 'not overrides exposure by default' do
526
+ subject.expose :name
527
+ child_class = Class.new(subject)
528
+ child_class.expose :name, as: :child_name
529
+
530
+ expect(subject.represent({ name: 'bar' }, serializable: true)).to eq(name: 'bar')
531
+ expect(child_class.represent({ name: 'bar' }, serializable: true)).to eq(name: 'bar', child_name: 'bar')
532
+ end
533
+
534
+ it 'overrides parent class exposure when option is specified' do
535
+ subject.expose :name
536
+ child_class = Class.new(subject)
537
+ child_class.expose :name, as: :child_name, override: true
538
+
539
+ expect(subject.represent({ name: 'bar' }, serializable: true)).to eq(name: 'bar')
540
+ expect(child_class.represent({ name: 'bar' }, serializable: true)).to eq(child_name: 'bar')
541
+ end
327
542
  end
328
543
 
329
544
  context 'register formatters' do
@@ -496,7 +711,7 @@ describe Grape::Entity do
496
711
  end
497
712
 
498
713
  exposure = subject.find_exposure(:awesome_thing)
499
- expect(exposure.key).to eq :extra_smooth
714
+ expect(exposure.key(subject)).to eq :extra_smooth
500
715
  end
501
716
 
502
717
  it 'merges nested :if option' do
@@ -596,6 +811,34 @@ describe Grape::Entity do
596
811
  exposure = subject.find_exposure(:awesome_thing)
597
812
  expect(exposure.documentation).to eq(desc: 'Other description.')
598
813
  end
814
+
815
+ it 'propagates expose_nil option' do
816
+ subject.class_eval do
817
+ with_options(expose_nil: false) do
818
+ expose :awesome_thing
819
+ end
820
+ end
821
+
822
+ exposure = subject.find_exposure(:awesome_thing)
823
+ expect(exposure.conditions[0].inversed?).to be true
824
+ expect(exposure.conditions[0].block.call(awesome_thing: nil)).to be true
825
+ end
826
+
827
+ it 'overrides nested :expose_nil option' do
828
+ subject.class_eval do
829
+ with_options(expose_nil: true) do
830
+ expose :awesome_thing, expose_nil: false
831
+ expose :other_awesome_thing
832
+ end
833
+ end
834
+
835
+ exposure = subject.find_exposure(:awesome_thing)
836
+ expect(exposure.conditions[0].inversed?).to be true
837
+ expect(exposure.conditions[0].block.call(awesome_thing: nil)).to be true
838
+ # Conditions are only added for exposures that do not expose nil
839
+ exposure = subject.find_exposure(:other_awesome_thing)
840
+ expect(exposure.conditions[0]).to be_nil
841
+ end
599
842
  end
600
843
 
601
844
  describe '.represent' do
@@ -653,7 +896,7 @@ describe Grape::Entity do
653
896
  context 'with specified fields' do
654
897
  it 'returns only specified fields with only option' do
655
898
  subject.expose(:id, :name, :phone)
656
- representation = subject.represent(OpenStruct.new, only: [:id, :name], serializable: true)
899
+ representation = subject.represent(OpenStruct.new, only: %i[id name], serializable: true)
657
900
  expect(representation).to eq(id: nil, name: nil)
658
901
  end
659
902
 
@@ -666,7 +909,7 @@ describe Grape::Entity do
666
909
  it 'returns only fields specified in the only option and not specified in the except option' do
667
910
  subject.expose(:id, :name, :phone)
668
911
  representation = subject.represent(OpenStruct.new,
669
- only: [:name, :phone],
912
+ only: %i[name phone],
670
913
  except: [:phone], serializable: true)
671
914
  expect(representation).to eq(name: nil)
672
915
  end
@@ -736,7 +979,7 @@ describe Grape::Entity do
736
979
  subject.expose(:id, :name, :phone)
737
980
  subject.expose(:user, using: user_entity)
738
981
 
739
- representation = subject.represent(OpenStruct.new(user: {}), only: [:id, :name, { user: [:name, :email] }], serializable: true)
982
+ representation = subject.represent(OpenStruct.new(user: {}), only: [:id, :name, { user: %i[name email] }], serializable: true)
740
983
  expect(representation).to eq(id: nil, name: nil, user: { name: nil, email: nil })
741
984
  end
742
985
 
@@ -759,7 +1002,7 @@ describe Grape::Entity do
759
1002
  subject.expose(:user, using: user_entity)
760
1003
 
761
1004
  representation = subject.represent(OpenStruct.new(user: {}),
762
- only: [:id, :name, :phone, user: [:id, :name, :email]],
1005
+ only: [:id, :name, :phone, { user: %i[id name email] }],
763
1006
  except: [:phone, { user: [:id] }], serializable: true)
764
1007
  expect(representation).to eq(id: nil, name: nil, user: { name: nil, email: nil })
765
1008
  end
@@ -771,7 +1014,7 @@ describe Grape::Entity do
771
1014
  subject.expose(:name)
772
1015
  end
773
1016
 
774
- representation = subject.represent(OpenStruct.new, condition: true, only: [:id, :name], serializable: true)
1017
+ representation = subject.represent(OpenStruct.new, condition: true, only: %i[id name], serializable: true)
775
1018
  expect(representation).to eq(id: nil, name: nil)
776
1019
  end
777
1020
 
@@ -781,7 +1024,7 @@ describe Grape::Entity do
781
1024
  subject.expose(:name, :mobile_phone)
782
1025
  end
783
1026
 
784
- representation = subject.represent(OpenStruct.new, condition: true, except: [:phone, :mobile_phone], serializable: true)
1027
+ representation = subject.represent(OpenStruct.new, condition: true, except: %i[phone mobile_phone], serializable: true)
785
1028
  expect(representation).to eq(id: nil, name: nil)
786
1029
  end
787
1030
 
@@ -863,7 +1106,7 @@ describe Grape::Entity do
863
1106
  subject.expose(:id)
864
1107
  subject.expose(:name, as: :title)
865
1108
 
866
- representation = subject.represent(OpenStruct.new, condition: true, only: [:id, :title], serializable: true)
1109
+ representation = subject.represent(OpenStruct.new, condition: true, only: %i[id title], serializable: true)
867
1110
  expect(representation).to eq(id: nil, title: nil)
868
1111
  end
869
1112
 
@@ -890,7 +1133,7 @@ describe Grape::Entity do
890
1133
  subject.expose(:nephew, using: nephew_entity)
891
1134
 
892
1135
  representation = subject.represent(OpenStruct.new(user: {}),
893
- only: [:id, :name, :user], except: [:nephew], serializable: true)
1136
+ only: %i[id name user], except: [:nephew], serializable: true)
894
1137
  expect(representation).to eq(id: nil, name: nil, user: { id: nil, name: nil, email: nil })
895
1138
  end
896
1139
  end
@@ -1178,6 +1421,18 @@ describe Grape::Entity do
1178
1421
  expect(res).to have_key :nonexistent_attribute
1179
1422
  end
1180
1423
 
1424
+ it 'exposes attributes defined through module inclusion' do
1425
+ module SharedAttributes
1426
+ def a_value
1427
+ 3.14
1428
+ end
1429
+ end
1430
+ fresh_class.include(SharedAttributes)
1431
+ fresh_class.expose :a_value
1432
+ res = fresh_class.new(model).serializable_hash
1433
+ expect(res[:a_value]).to eq(3.14)
1434
+ end
1435
+
1181
1436
  it 'does not expose attributes that are generated by a block but have not passed criteria' do
1182
1437
  fresh_class.expose :nonexistent_attribute,
1183
1438
  proc: ->(_, _) { 'I exist, but it is not yet my time to shine' },
@@ -1341,8 +1596,8 @@ describe Grape::Entity do
1341
1596
  it 'allows to pass different :only and :except params using the same instance' do
1342
1597
  fresh_class.expose :a, :b, :c
1343
1598
  presenter = fresh_class.new(a: 1, b: 2, c: 3)
1344
- expect(presenter.serializable_hash(only: [:a, :b])).to eq(a: 1, b: 2)
1345
- expect(presenter.serializable_hash(only: [:b, :c])).to eq(b: 2, c: 3)
1599
+ expect(presenter.serializable_hash(only: %i[a b])).to eq(a: 1, b: 2)
1600
+ expect(presenter.serializable_hash(only: %i[b c])).to eq(b: 2, c: 3)
1346
1601
  end
1347
1602
  end
1348
1603
  end
@@ -1463,10 +1718,12 @@ describe Grape::Entity do
1463
1718
  end
1464
1719
  end
1465
1720
 
1721
+ # rubocop:disable Lint/EmptyBlock
1466
1722
  fresh_class.class_eval do
1467
1723
  expose :first_friend, using: EntitySpec::FriendEntity do |_user, _opts|
1468
1724
  end
1469
1725
  end
1726
+ # rubocop:enable Lint/EmptyBlock
1470
1727
 
1471
1728
  rep = subject.value_for(:first_friend)
1472
1729
  expect(rep).to be_kind_of EntitySpec::FriendEntity
@@ -1765,39 +2022,5 @@ describe Grape::Entity do
1765
2022
  end
1766
2023
  end
1767
2024
  end
1768
-
1769
- describe Grape::Entity::Options do
1770
- module EntitySpec
1771
- class Crystalline
1772
- attr_accessor :prop1, :prop2
1773
-
1774
- def initialize
1775
- @prop1 = 'value1'
1776
- @prop2 = 'value2'
1777
- end
1778
- end
1779
-
1780
- class CrystallineEntity < Grape::Entity
1781
- expose :prop1, if: ->(_, options) { options.fetch(:signal) }
1782
- expose :prop2, if: ->(_, options) { options.fetch(:beam, 'destructive') == 'destructive' }
1783
- end
1784
- end
1785
-
1786
- context '#fetch' do
1787
- it 'without passing in a required option raises KeyError' do
1788
- expect { EntitySpec::CrystallineEntity.represent(EntitySpec::Crystalline.new).as_json }.to raise_error KeyError
1789
- end
1790
-
1791
- it 'passing in a required option will expose the values' do
1792
- crystalline_entity = EntitySpec::CrystallineEntity.represent(EntitySpec::Crystalline.new, signal: true)
1793
- expect(crystalline_entity.as_json).to eq(prop1: 'value1', prop2: 'value2')
1794
- end
1795
-
1796
- it 'with an option that is not default will not expose that value' do
1797
- crystalline_entity = EntitySpec::CrystallineEntity.represent(EntitySpec::Crystalline.new, signal: true, beam: 'intermittent')
1798
- expect(crystalline_entity.as_json).to eq(prop1: 'value1')
1799
- end
1800
- end
1801
- end
1802
2025
  end
1803
2026
  end