grape-entity 0.6.1 → 0.8.2

Sign up to get free protection for your applications and to get access to all the features.
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