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.
- checksums.yaml +5 -5
- data/.coveralls.yml +1 -0
- data/.gitignore +5 -1
- data/.rubocop.yml +82 -2
- data/.rubocop_todo.yml +16 -33
- data/.travis.yml +18 -17
- data/CHANGELOG.md +75 -0
- data/Dangerfile +2 -0
- data/Gemfile +6 -1
- data/Guardfile +4 -2
- data/README.md +101 -4
- data/Rakefile +2 -2
- data/UPGRADING.md +31 -2
- data/bench/serializing.rb +7 -0
- data/grape-entity.gemspec +10 -10
- data/lib/grape-entity.rb +2 -0
- data/lib/grape_entity.rb +3 -0
- data/lib/grape_entity/condition.rb +20 -11
- data/lib/grape_entity/condition/base.rb +3 -1
- data/lib/grape_entity/condition/block_condition.rb +3 -1
- data/lib/grape_entity/condition/hash_condition.rb +2 -0
- data/lib/grape_entity/condition/symbol_condition.rb +2 -0
- data/lib/grape_entity/delegator.rb +10 -9
- data/lib/grape_entity/delegator/base.rb +2 -0
- data/lib/grape_entity/delegator/fetchable_object.rb +2 -0
- data/lib/grape_entity/delegator/hash_object.rb +4 -2
- data/lib/grape_entity/delegator/openstruct_object.rb +2 -0
- data/lib/grape_entity/delegator/plain_object.rb +2 -0
- data/lib/grape_entity/deprecated.rb +13 -0
- data/lib/grape_entity/entity.rb +115 -36
- data/lib/grape_entity/exposure.rb +64 -41
- data/lib/grape_entity/exposure/base.rb +21 -8
- data/lib/grape_entity/exposure/block_exposure.rb +2 -0
- data/lib/grape_entity/exposure/delegator_exposure.rb +2 -0
- data/lib/grape_entity/exposure/formatter_block_exposure.rb +2 -0
- data/lib/grape_entity/exposure/formatter_exposure.rb +2 -0
- data/lib/grape_entity/exposure/nesting_exposure.rb +36 -30
- data/lib/grape_entity/exposure/nesting_exposure/nested_exposures.rb +26 -15
- data/lib/grape_entity/exposure/nesting_exposure/output_builder.rb +10 -2
- data/lib/grape_entity/exposure/represent_exposure.rb +3 -1
- data/lib/grape_entity/options.rb +44 -58
- data/lib/grape_entity/version.rb +3 -1
- data/spec/grape_entity/entity_spec.rb +270 -47
- data/spec/grape_entity/exposure/nesting_exposure/nested_exposures_spec.rb +6 -4
- data/spec/grape_entity/exposure/represent_exposure_spec.rb +5 -3
- data/spec/grape_entity/exposure_spec.rb +14 -2
- data/spec/grape_entity/hash_spec.rb +38 -1
- data/spec/grape_entity/options_spec.rb +66 -0
- data/spec/spec_helper.rb +17 -0
- 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
|
@@ -29,45 +31,33 @@ module Grape
|
|
29
31
|
end
|
30
32
|
|
31
33
|
def value(entity, options)
|
32
|
-
|
33
|
-
|
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
|
44
|
-
|
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
|
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).
|
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
|
-
|
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
|
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
|
-
|
101
|
-
output[exposure.key]
|
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
|
-
|
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
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
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 =
|
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
|
|
data/lib/grape_entity/options.rb
CHANGED
@@ -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
|
15
|
-
|
16
|
-
end
|
22
|
+
def merge(new_opts)
|
23
|
+
return self if new_opts.empty?
|
17
24
|
|
18
|
-
|
19
|
-
|
20
|
-
|
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
|
-
|
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
|
-
|
53
|
-
|
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
|
-
|
58
|
-
|
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
|
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
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
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
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
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
|
-
|
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
|
-
|
114
|
+
when Array
|
129
115
|
return attribute.each { |x| build_symbolized_hash(x, {}) }
|
130
116
|
else
|
131
117
|
hash[attribute.to_sym] = true
|
data/lib/grape_entity/version.rb
CHANGED
@@ -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
|
-
|
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: [
|
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: [
|
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: [
|
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: [
|
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: [
|
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: [
|
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: [
|
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: [
|
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: [
|
1345
|
-
expect(presenter.serializable_hash(only: [
|
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
|